diff --git a/.github/workflows/CI-checks.yml b/.github/workflows/CI-checks.yml new file mode 100644 index 0000000..c24a9bc --- /dev/null +++ b/.github/workflows/CI-checks.yml @@ -0,0 +1,63 @@ +name: CI integrity checks + +on: + pull_request: + branches: + - main + - dev + push: + branches: + - main + +jobs: + backend-checks: + name: Backend integrity checks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.25' + cache: true + + - name: Download Dependencies + run: go mod download + + - name: Verify Backend Build + run: go build -v ./... + + - name: Run Tests + run: go test -v ./... + + frontend-checks: + name: Frontend integrity checks + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./lb-web + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: lb-web/package-lock.json + + - name: Install Dependencies + run: npm ci + + - name: Type check & lint + run: npm run lint + continue-on-error: true + + - name: Run Tests + run: npm run test + + - name: Build + run: npm run build \ No newline at end of file diff --git a/Internal/adapters/repository/authRepo.go b/Internal/adapters/repository/authRepo.go new file mode 100644 index 0000000..d4c80fb --- /dev/null +++ b/Internal/adapters/repository/authRepo.go @@ -0,0 +1,26 @@ +package repository + +import "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" + +type AuthRepo struct { + DB *DBContainer +} + +func NewAuthRepo(db *DBContainer) *AuthRepo { + return &AuthRepo{DB: db} +} + +func (ar *AuthRepo) RegisterRepo(user *domain.User) error { + if err := ar.DB.Gorm.Create(user).Error; err != nil { + return err + } + return nil +} + +func (ar *AuthRepo) LoginRepo(email string) (*domain.User, error) { + var user domain.User + if err := ar.DB.Gorm.Where("email = ?", email).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} diff --git a/Internal/adapters/repository/postgres.go b/Internal/adapters/repository/postgres.go new file mode 100644 index 0000000..bc02520 --- /dev/null +++ b/Internal/adapters/repository/postgres.go @@ -0,0 +1,49 @@ +package repository + +import ( + "fmt" + "log" + + "github.com/Arjuna-Ragil/Localbase/Internal/config" + "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type DBContainer struct { + Gorm *gorm.DB + Sqlx *sqlx.DB +} + +func ConnectDB() (*DBContainer, error) { + cfg := config.LoadConfig() + + gormDB, err := gorm.Open(postgres.Open(cfg.DBURL), &gorm.Config{}) + if err != nil { + log.Fatalf("Failed to connect to gorm database: %v", err) + } + fmt.Println("Connected to gorm database") + + sqlxDB, err := sqlx.Connect("postgres", cfg.DBURL) + if err != nil { + log.Fatalf("Failed to connect to sqlx database: %v", err) + } + fmt.Println("Connected to sqlx database") + + return &DBContainer{ + Gorm: gormDB, + Sqlx: sqlxDB, + }, nil +} +func (db *DBContainer) Migrate() error { + err := db.Gorm.AutoMigrate( + &domain.User{}, + ) + if err != nil { + log.Fatalf("Failed to migrate users: %v", err) + } + fmt.Println("Migrated users") + return nil +} diff --git a/Internal/adapters/repository/sysRepo.go b/Internal/adapters/repository/sysRepo.go new file mode 100644 index 0000000..717c815 --- /dev/null +++ b/Internal/adapters/repository/sysRepo.go @@ -0,0 +1,19 @@ +package repository + +import "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" + +type SystemRepo struct { + DB *DBContainer +} + +func NewSystemRepo(db *DBContainer) *SystemRepo { + return &SystemRepo{DB: db} +} + +func (sr *SystemRepo) IsAppInitialized() (bool, error) { + var count int64 + if err := sr.DB.Gorm.Model(&domain.User{}).Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} diff --git a/Internal/adapters/repository/userRepo.go b/Internal/adapters/repository/userRepo.go new file mode 100644 index 0000000..db3fe87 --- /dev/null +++ b/Internal/adapters/repository/userRepo.go @@ -0,0 +1,30 @@ +package repository + +import "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" + +type UserQuery interface { + Save(user *domain.User) error + FindById(id string) (*domain.User, error) +} +type UserRepository struct { + db *DBContainer +} + +func NewUserRepository(db *DBContainer) *UserRepository { + return &UserRepository{db: db} +} + +// Query + +func (ur *UserRepository) save(user *domain.User) error { + return ur.db.Gorm.Create(user).Error +} + +func (ur *UserRepository) FindById(id uint) (*domain.User, error) { + var user domain.User + err := ur.db.Gorm.First(&user, id).Error + if err != nil { + return nil, err + } + return &user, nil +} diff --git a/Internal/api/Routes/routev1.go b/Internal/api/Routes/routev1.go new file mode 100644 index 0000000..db257b4 --- /dev/null +++ b/Internal/api/Routes/routev1.go @@ -0,0 +1,45 @@ +package Routes + +import ( + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + "github.com/Arjuna-Ragil/Localbase/Internal/api/handlers" + "github.com/Arjuna-Ragil/Localbase/Internal/api/middleware" + "github.com/Arjuna-Ragil/Localbase/Internal/config" + "github.com/gin-gonic/gin" +) + +type Deps struct { + User *handlers.UserHandler + Auth *handlers.AuthHandler + System *handlers.SystemHandler + UserRepo *repository.UserRepository + Config *config.Config +} + +func SetupRouterV1(r *gin.Engine, deps Deps) { + api := r.Group("/api/v1") + + authMiddleware := middleware.AuthMiddleware(deps.UserRepo, deps.Config) + { + system := api.Group("/system") + { + system.GET("/status", deps.System.CheckAppInitStatus) + } + + auth := api.Group("/auth") + { + auth.POST("/Register", deps.Auth.RegisterHandler) + auth.POST("/Login", deps.Auth.LoginHandler) + auth.POST("/logout", deps.Auth.LogoutHandler) + } + + protected := api.Group("/protected") + protected.Use(authMiddleware) + { + user := protected.Group("/user") + { + user.GET("/me", deps.User.GetUserHandler) + } + } + } +} diff --git a/Internal/api/handlers/authHandler.go b/Internal/api/handlers/authHandler.go new file mode 100644 index 0000000..c6a3932 --- /dev/null +++ b/Internal/api/handlers/authHandler.go @@ -0,0 +1,71 @@ +package handlers + +import ( + "net/http" + + "github.com/Arjuna-Ragil/Localbase/Internal/core/services" + "github.com/gin-gonic/gin" +) + +type AuthHandler struct { + Service *services.AuthService +} + +func NewAuthHandler(service *services.AuthService) *AuthHandler { + return &AuthHandler{Service: service} +} + +func (ah *AuthHandler) RegisterHandler(c *gin.Context) { + var input services.RegisterInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Invalid Input", + "data": err.Error(), + }) + return + } + if err := ah.Service.RegisterService(&input); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": "Register service failed", + "data": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "message": "Register service success", + "data": nil, + }) +} + +func (ah *AuthHandler) LoginHandler(c *gin.Context) { + var input services.LoginInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Invalid Input", + "data": err.Error(), + }) + return + } + tokenString, err := ah.Service.LoginService(&input) + if err != nil { + c.JSON(401, gin.H{ + "message": "Login failed", + "data": err.Error(), + }) + return + } + c.SetSameSite(http.SameSiteLaxMode) + c.SetCookie("authorization", tokenString, 3600*24, "/", "", false, true) + c.JSON(http.StatusOK, gin.H{ + "message": "Login success", + "data": tokenString, + }) +} + +func (ah *AuthHandler) LogoutHandler(c *gin.Context) { + c.SetCookie("authorization", "", -1, "/", "", false, true) + c.JSON(http.StatusOK, gin.H{ + "message": "Logout success", + "data": nil, + }) +} diff --git a/Internal/api/handlers/sysHandler.go b/Internal/api/handlers/sysHandler.go new file mode 100644 index 0000000..20e54a5 --- /dev/null +++ b/Internal/api/handlers/sysHandler.go @@ -0,0 +1,24 @@ +package handlers + +import ( + "net/http" + + "github.com/Arjuna-Ragil/Localbase/Internal/core/services" + "github.com/gin-gonic/gin" +) + +type SystemHandler struct { + Service *services.SystemService +} + +func NewSystemHandler(service *services.SystemService) *SystemHandler { + return &SystemHandler{Service: service} +} + +func (sh *SystemHandler) CheckAppInitStatus(c *gin.Context) { + isInit, _ := sh.Service.CheckAppInitialized() + c.JSON(http.StatusOK, gin.H{ + "message": "App init status", + "data": isInit, + }) +} diff --git a/Internal/api/handlers/userHandler.go b/Internal/api/handlers/userHandler.go new file mode 100644 index 0000000..897e371 --- /dev/null +++ b/Internal/api/handlers/userHandler.go @@ -0,0 +1,37 @@ +package handlers + +import ( + "github.com/Arjuna-Ragil/Localbase/Internal/core/services" + "github.com/gin-gonic/gin" +) + +type UserHandler struct { + Service *services.UserService +} + +func NewUserHandler(s *services.UserService) *UserHandler { + return &UserHandler{Service: s} +} + +func (uh *UserHandler) GetUserHandler(c *gin.Context) { + userID, exist := c.Get("userID") + if !exist { + c.JSON(400, gin.H{ + "message": "Unable to find user", + "data": exist, + }) + return + } + user, err := uh.Service.GetUser(userID.(uint)) + if err != nil { + c.JSON(400, gin.H{ + "message": "Unable to get user data", + "data": err, + }) + return + } + c.JSON(200, gin.H{ + "message": "Successfully get user data", + "data": user, + }) +} diff --git a/Internal/api/middleware/auth.go b/Internal/api/middleware/auth.go new file mode 100644 index 0000000..d274e69 --- /dev/null +++ b/Internal/api/middleware/auth.go @@ -0,0 +1,88 @@ +package middleware + +import ( + "fmt" + "strings" + "time" + + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + "github.com/Arjuna-Ragil/Localbase/Internal/config" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +func AuthMiddleware(userRepo *repository.UserRepository, cfg *config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + if cfg.AuthMode == "false" { + c.Set("userID", 1) + c.Next() + return + } + tokenString, err := c.Cookie("authorization") + if tokenString == "" { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(401, gin.H{ + "message": "Authorization header not found", + "data": err.Error(), + }) + return + } + parts := strings.Split(authHeader, " ") + if len(parts) == 2 { + tokenString = parts[1] + } else { + tokenString = authHeader + } + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("method unknown") + } + return []byte(cfg.SecretKey), nil + }) + + if err != nil || !token.Valid { + c.JSON(401, gin.H{ + "message": "Invalid token", + "data": err.Error(), + }) + return + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + if float64(time.Now().Unix()) > claims["exp"].(float64) { + c.JSON(401, gin.H{ + "message": "Token expired", + "data": err.Error(), + }) + return + } + subID := uint(claims["sub"].(float64)) + user, err := userRepo.FindById(subID) + if err != nil { + c.JSON(401, gin.H{ + "message": "User not found", + "data": err.Error(), + }) + return + } + + c.Set("userID", user.Id) + c.Set("userRole", user.Role) + c.Next() + + c.JSON(200, gin.H{ + "message": "Success", + "data": " ", + }) + } else { + c.JSON(401, gin.H{ + "message": "Invalid token", + "data": err.Error(), + }) + return + } + } +} diff --git a/Internal/config/config.go b/Internal/config/config.go index 010aa67..7b19b63 100644 --- a/Internal/config/config.go +++ b/Internal/config/config.go @@ -12,6 +12,8 @@ type Config struct { Port string DBURL string LedgerPath string + SecretKey string + AuthMode string } func LoadConfig() Config { @@ -23,6 +25,8 @@ func LoadConfig() Config { AppEnv: getEnv("APP_ENV", "local"), Port: getEnv("PORT", "8080"), LedgerPath: getEnv("LEDGER_PATH", "../Storage/ledger.db"), + SecretKey: getEnv("SECRET_KEY", "secret"), + AuthMode: getEnv("AUTH_MODE", "true"), DBURL: "host=" + getEnv("DB_HOST", "localhost") + " user=" + getEnv("DB_USER", "postgres") + " password=" + getEnv("DB_PASSWORD", "password") + diff --git a/Internal/core/domain/userdb.go b/Internal/core/domain/userdb.go new file mode 100644 index 0000000..110598d --- /dev/null +++ b/Internal/core/domain/userdb.go @@ -0,0 +1,39 @@ +package domain + +import ( + "time" + + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +type User struct { + Id uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` + Email string `gorm:"size:255;unique;not null" json:"email"` + Password string `json:"-"` + Username string `gorm:"size:255;unique;not null" json:"username"` + Role string `gorm:"size:255;not null" json:"role"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` + UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` +} + +func (u *User) BeforeCreate(tx *gorm.DB) (err error) { + if len(u.Password) < 50 { + hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + u.Password = string(hashed) + } + var count int64 + tx.Model(&User{}).Count(&count) + if count == 0 { + u.Role = "admin" + } else { + if u.Role == "" { + u.Role = "user" + } + } + + return nil +} diff --git a/Internal/core/services/authService.go b/Internal/core/services/authService.go new file mode 100644 index 0000000..848445e --- /dev/null +++ b/Internal/core/services/authService.go @@ -0,0 +1,66 @@ +package services + +import ( + "time" + + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + "github.com/Arjuna-Ragil/Localbase/Internal/config" + "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +type AuthService struct { + Repo *repository.AuthRepo +} + +type RegisterInput struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + Role string `json:"role" binding:"required"` +} + +type LoginInput struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` +} + +func NewAuthService(repo *repository.AuthRepo) *AuthService { + return &AuthService{Repo: repo} +} + +func (as *AuthService) RegisterService(input *RegisterInput) error { + user := domain.User{ + Username: input.Username, + Email: input.Email, + Password: input.Password, + Role: input.Role, + } + err := as.Repo.RegisterRepo(&user) + if err != nil { + return err + } + return nil +} + +func (as *AuthService) LoginService(input *LoginInput) (string, error) { + cfg := config.LoadConfig() + + user, err := as.Repo.LoginRepo(input.Email) + if err != nil { + return "", err + } + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil { + return "", err + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": user.Id, + "exp": time.Now().Add(time.Hour * 48).Unix(), + }) + tokenString, err := token.SignedString([]byte(cfg.SecretKey)) + if err != nil { + return "", err + } + return tokenString, nil +} diff --git a/Internal/core/services/sysService.go b/Internal/core/services/sysService.go new file mode 100644 index 0000000..9242170 --- /dev/null +++ b/Internal/core/services/sysService.go @@ -0,0 +1,19 @@ +package services + +import "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + +type SystemService struct { + Repo *repository.SystemRepo +} + +func NewSystemService(repo *repository.SystemRepo) *SystemService { + return &SystemService{Repo: repo} +} + +func (ss *SystemService) CheckAppInitialized() (bool, error) { + isInitialized, err := ss.Repo.IsAppInitialized() + if err != nil { + return false, err + } + return isInitialized, nil +} diff --git a/Internal/core/services/userService.go b/Internal/core/services/userService.go new file mode 100644 index 0000000..60d7146 --- /dev/null +++ b/Internal/core/services/userService.go @@ -0,0 +1,24 @@ +package services + +import ( + "errors" + + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" +) + +type UserService struct { + Repo *repository.UserRepository +} + +func NewUserService(repo *repository.UserRepository) *UserService { + return &UserService{Repo: repo} +} + +func (us *UserService) GetUser(id uint) (*domain.User, error) { + user, err := us.Repo.FindById(id) + if err != nil { + return nil, errors.New("user not found") + } + return user, nil +} diff --git a/Lb-web/package-lock.json b/Lb-web/package-lock.json index 87e67ea..311c25b 100644 --- a/Lb-web/package-lock.json +++ b/Lb-web/package-lock.json @@ -8,12 +8,16 @@ "name": "lb-web", "version": "0.0.0", "dependencies": { + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.18", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router": "^7.12.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18" }, @@ -943,6 +947,85 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", @@ -1785,7 +1868,7 @@ "version": "19.2.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1795,7 +1878,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -2150,6 +2233,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2212,6 +2312,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2301,6 +2414,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2315,6 +2440,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2334,7 +2472,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -2362,6 +2500,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2371,6 +2518,20 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -2391,6 +2552,51 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -2728,6 +2934,42 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2742,6 +2984,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2752,6 +3003,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2778,6 +3066,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2794,6 +3094,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -3278,6 +3617,36 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3469,6 +3838,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3500,6 +3875,28 @@ "react": "^19.2.3" } }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3570,6 +3967,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/Lb-web/package.json b/Lb-web/package.json index 92e0666..896865c 100644 --- a/Lb-web/package.json +++ b/Lb-web/package.json @@ -10,12 +10,16 @@ "preview": "vite preview" }, "dependencies": { + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.18", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router": "^7.12.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18" }, diff --git a/Lb-web/public/Localbase Logo.svg b/Lb-web/public/Localbase Logo.svg new file mode 100644 index 0000000..b56ef46 --- /dev/null +++ b/Lb-web/public/Localbase Logo.svg @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/Lb-web/public/vite.svg b/Lb-web/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/Lb-web/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Lb-web/src/App.tsx b/Lb-web/src/App.tsx deleted file mode 100644 index e677b9b..0000000 --- a/Lb-web/src/App.tsx +++ /dev/null @@ -1,9 +0,0 @@ -function App() { - return ( - <> - test - - ) -} - -export default App diff --git a/Lb-web/src/components/ui/button.tsx b/Lb-web/src/components/ui/button.tsx new file mode 100644 index 0000000..f22ed06 --- /dev/null +++ b/Lb-web/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/Lb-web/src/components/ui/card.tsx b/Lb-web/src/components/ui/card.tsx new file mode 100644 index 0000000..cedfc91 --- /dev/null +++ b/Lb-web/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/Lb-web/src/components/ui/input.tsx b/Lb-web/src/components/ui/input.tsx new file mode 100644 index 0000000..2bd6a9f --- /dev/null +++ b/Lb-web/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/Lb-web/src/components/ui/label.tsx b/Lb-web/src/components/ui/label.tsx new file mode 100644 index 0000000..ef090dc --- /dev/null +++ b/Lb-web/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/Lb-web/src/features/Login/components/AuthWrappers.tsx b/Lb-web/src/features/Login/components/AuthWrappers.tsx new file mode 100644 index 0000000..cca6571 --- /dev/null +++ b/Lb-web/src/features/Login/components/AuthWrappers.tsx @@ -0,0 +1,30 @@ +import { Navigate, Outlet } from "react-router"; +import { useAuth } from "../context/AuthContext"; + +export const ProtectedRoute = () => { + const { isAuthenticated, loading } = useAuth(); + + if (loading) { + return
Loading...
; + } + + if (!isAuthenticated) { + return ; + } + + return ; +}; + +export const PublicRoute = () => { + const { isAuthenticated, loading } = useAuth(); + + if (loading) { + return
Loading...
; + } + + if (isAuthenticated) { + return ; + } + + return ; +}; diff --git a/Lb-web/src/features/Login/context/AuthContext.tsx b/Lb-web/src/features/Login/context/AuthContext.tsx new file mode 100644 index 0000000..7c2e2dc --- /dev/null +++ b/Lb-web/src/features/Login/context/AuthContext.tsx @@ -0,0 +1,59 @@ +import { createContext, useContext, useEffect, useState, type ReactNode } from "react"; +import api from "@/services/api"; + +interface AuthContextType { + isAuthenticated: boolean | null; + loading: boolean; + login: () => void; // call this after successful login to update state + logout: () => Promise; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [isAuthenticated, setIsAuthenticated] = useState(null); + const [loading, setLoading] = useState(true); + + const checkAuth = async () => { + try { + await api.get('/protected/user/me'); + setIsAuthenticated(true); + } catch { + setIsAuthenticated(false); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + checkAuth(); + }, []); + + const login = () => { + setIsAuthenticated(true); + }; + + const logout = async () => { + try { + await api.post('/auth/logout'); + } catch (error) { + console.error("Logout failed", error); + } finally { + setIsAuthenticated(false); + } + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/Lb-web/src/features/Login/hooks/useLogin.ts b/Lb-web/src/features/Login/hooks/useLogin.ts new file mode 100644 index 0000000..70192eb --- /dev/null +++ b/Lb-web/src/features/Login/hooks/useLogin.ts @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import api from '@/services/api'; +import { useAuth } from '../context/AuthContext'; + +export const useLogin = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { login } = useAuth(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + await api.post('/auth/Login', { email, password }); + // Redirect or update global auth state here + // For now, assuming successful cookie set by backend + login(); // Update auth state, wrapper will handle redirect + console.log("Login successful"); + } catch (err: any) { + console.error("Login failed", err); + if (err.response && err.response.data && err.response.data.message) { + setError(err.response.data.message); + } else { + setError('Something went wrong. Please try again.'); + } + } finally { + setLoading(false); + } + }; + + return { + email, + setEmail, + password, + setPassword, + loading, + error, + handleLogin, + }; +}; diff --git a/Lb-web/src/features/Login/hooks/useRegister.ts b/Lb-web/src/features/Login/hooks/useRegister.ts new file mode 100644 index 0000000..cf3d13e --- /dev/null +++ b/Lb-web/src/features/Login/hooks/useRegister.ts @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import api from '@/services/api'; +import { useNavigate } from 'react-router'; + +export const useRegister = () => { + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [role, setRole] = useState('admin'); // Default to admin for first user? Or let backend handle it? + // Assuming for first run, the user being created is likely an Admin/Owner. + // The backend might enforce this or we can just send it. + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + const handleRegister = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + await api.post('/auth/Register', { + username, + email, + password, + role + }); + console.log("Registration successful"); + // After successful registration, usually redirect to login or auto-login. + // For now, redirect to login page (which should now be accessible since system is init) + navigate('/login'); + } catch (err: any) { + console.error("Registration failed", err); + if (err.response && err.response.data && err.response.data.message) { + setError(err.response.data.message); + } else { + setError('Something went wrong. Please try again.'); + } + } finally { + setLoading(false); + } + }; + + return { + username, + setUsername, + email, + setEmail, + password, + setPassword, + role, + setRole, + loading, + error, + handleRegister, + }; +}; diff --git a/Lb-web/src/features/Login/login.tsx b/Lb-web/src/features/Login/login.tsx new file mode 100644 index 0000000..03d830e --- /dev/null +++ b/Lb-web/src/features/Login/login.tsx @@ -0,0 +1,84 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useLogin } from "./hooks/useLogin"; + +export default function Login() { + const { email, setEmail, password, setPassword, loading, error, handleLogin } = useLogin(); + + return ( +
+ {/* Dynamic Background Elements */} +
+
+
+ + + +
+ Localbase Logo +
+ + Welcome back + + + Enter your email to sign in to your account + +
+
+ + {error && ( +
+ {error} +
+ )} +
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+
+ + + +
+
+
+ ); +} diff --git a/Lb-web/src/features/Login/register.tsx b/Lb-web/src/features/Login/register.tsx new file mode 100644 index 0000000..b692fce --- /dev/null +++ b/Lb-web/src/features/Login/register.tsx @@ -0,0 +1,112 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useRegister } from "./hooks/useRegister"; + +export default function Register() { + const { + username, setUsername, + email, setEmail, + password, setPassword, + role, setRole, + loading, error, handleRegister + } = useRegister(); + + return ( +
+ {/* Dynamic Background Elements */} +
+
+
+ + + +
+ Localbase Logo +
+ + Create an Account + + + Setup your initial administrator account + +
+
+ + {error && ( +
+ {error} +
+ )} +
+ + setUsername(e.target.value)} + required + /> +
+
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ {/* Hidden Role Input - defaulting to admin/input value */} +
+ setRole(e.target.value)} + /> +
+ +
+ + + +
+
+
+ ); +} diff --git a/Lb-web/src/index.css b/Lb-web/src/index.css index 9d01a7b..0553fb4 100644 --- a/Lb-web/src/index.css +++ b/Lb-web/src/index.css @@ -42,6 +42,26 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + + --animate-blob: blob 7s infinite; + + @keyframes blob { + 0% { + transform: translate(0px, 0px) scale(1); + } + + 33% { + transform: translate(30px, -50px) scale(1.1); + } + + 66% { + transform: translate(-20px, 20px) scale(0.9); + } + + 100% { + transform: translate(0px, 0px) scale(1); + } + } } :root { @@ -117,6 +137,7 @@ * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; } diff --git a/Lb-web/src/main.tsx b/Lb-web/src/main.tsx index bef5202..bd46efe 100644 --- a/Lb-web/src/main.tsx +++ b/Lb-web/src/main.tsx @@ -1,10 +1,41 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' -import App from './App.tsx' +import Login from './features/Login/login.tsx' +import Register from './features/Login/register.tsx' +import Home from './pages/Home.tsx' +import { createBrowserRouter, RouterProvider } from 'react-router' +import { AuthProvider } from './features/Login/context/AuthContext.tsx' +import { ProtectedRoute, PublicRoute } from './features/Login/components/AuthWrappers.tsx' + +const router = createBrowserRouter([ + { + element: , + children: [ + { + path: 'login', + element: + }, + { + path: 'register', + element: + } + ] + }, + { + path: '/', + element: , + children: [ + { + index: true, + element: + } + ] + } +]) createRoot(document.getElementById('root')!).render( - + , ) diff --git a/Lb-web/src/pages/Home.tsx b/Lb-web/src/pages/Home.tsx new file mode 100644 index 0000000..4b4844a --- /dev/null +++ b/Lb-web/src/pages/Home.tsx @@ -0,0 +1,86 @@ +import { Button } from "@/components/ui/button"; +import { User, Settings, Plus, LogOut } from "lucide-react"; +import { useState } from "react"; +import { useAuth } from "@/features/Login/context/AuthContext"; + +export default function Home() { + const { logout } = useAuth(); + const [isProfileOpen, setIsProfileOpen] = useState(false); + + const handleLogout = async () => { + await logout(); + }; + + return ( +
+ {/* Subtle Grid & Gradient Background */} +
+ +
+
+
+ + {/* Navbar */} +
+
+ Localbase Logo +

Localbase

+
+
+
+ + + {isProfileOpen && ( + <> + {/* Transparent backdrop to close on click outside */} +
setIsProfileOpen(false)}>
+ +
+
+ +
+
+ + )} +
+ +
+
+ + {/* Main Content */} +
+
+
+
+ +
+

There is no Project yet

+

Create your first project to get started with Localbase.

+ +
+
+
+
+ ); +} diff --git a/Lb-web/src/services/api.ts b/Lb-web/src/services/api.ts new file mode 100644 index 0000000..99c00c1 --- /dev/null +++ b/Lb-web/src/services/api.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: 'http://localhost:8080/api/v1', + withCredentials: true, // Important for cookies +}); + +// Optional: Add interceptor to handle 401s globally +api.interceptors.response.use( + (response) => response, + (error) => { + return Promise.reject(error); + } +); + +export default api; diff --git a/cmd/server/main.go b/cmd/server/main.go index ca2a26b..a5dc1df 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -4,29 +4,67 @@ import ( "fmt" "log" + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + "github.com/Arjuna-Ragil/Localbase/Internal/api/Routes" + "github.com/Arjuna-Ragil/Localbase/Internal/api/handlers" "github.com/Arjuna-Ragil/Localbase/Internal/config" + "github.com/Arjuna-Ragil/Localbase/Internal/core/services" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main() { cfg := config.LoadConfig() - fmt.Println("Starting Localbase at port:", cfg.Port) if cfg.AppEnv == "production" { gin.SetMode(gin.ReleaseMode) } + + db, err := repository.ConnectDB() + if err != nil { + log.Fatalf("Error connecting to database: %v", err) + } + if err := db.Migrate(); err != nil { + log.Fatalf("error migrating database: %v", err) + } + r := gin.Default() r.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://localhost:5173"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, AllowCredentials: true, })) + deps := SetupApp(db, &cfg) + + Routes.SetupRouterV1(r, deps) + serverPath := fmt.Sprintf(":%s", cfg.Port) if err := r.Run(serverPath); err != nil { log.Fatalf("Failed to start server: %v", err) } } + +func SetupApp(db *repository.DBContainer, cfg *config.Config) Routes.Deps { + userRepo := repository.NewUserRepository(db) + userService := services.NewUserService(userRepo) + userHandler := handlers.NewUserHandler(userService) + + authRepo := repository.NewAuthRepo(db) + authService := services.NewAuthService(authRepo) + authHandler := handlers.NewAuthHandler(authService) + + systemRepo := repository.NewSystemRepo(db) + systemService := services.NewSystemService(systemRepo) + systemHandler := handlers.NewSystemHandler(systemService) + + return Routes.Deps{ + User: userHandler, + Auth: authHandler, + System: systemHandler, + UserRepo: userRepo, + Config: cfg, + } +} diff --git a/go.mod b/go.mod index 03e78ec..23d07ae 100644 --- a/go.mod +++ b/go.mod @@ -2,31 +2,38 @@ module github.com/Arjuna-Ragil/Localbase go 1.25.4 +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/jmoiron/sqlx v1.4.0 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 +) + require ( github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect - github.com/gin-contrib/cors v1.7.6 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.11.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.8.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -38,13 +45,9 @@ require ( go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.23.0 // indirect golang.org/x/crypto v0.47.0 // indirect - golang.org/x/mod v0.32.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect - golang.org/x/tools v0.41.0 // indirect google.golang.org/protobuf v1.36.11 // indirect - gorm.io/driver/postgres v1.6.0 // indirect - gorm.io/gorm v1.31.1 // indirect ) diff --git a/go.sum b/go.sum index cfe559e..32c3108 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= @@ -7,6 +9,7 @@ github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCc github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= @@ -16,16 +19,24 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -39,6 +50,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -51,6 +64,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -58,6 +73,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= @@ -73,6 +89,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= @@ -83,8 +101,6 @@ golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= @@ -94,12 +110,11 @@ golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=