diff --git a/docker-compose.yml b/docker-compose.yml index 56e94eab..3d62d33d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,10 +10,13 @@ services: - "${DB_PORT}:5432" volumes: - psql_volume_bp:/var/lib/postgresql/data + networks: + - dev_network minio: image: minio/minio:latest environment: + MINIO_SERVER_URL: http://localhost:${MINIO_PORT:-9000} MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} command: server /data @@ -22,6 +25,8 @@ services: volumes: - minio_data:/data restart: unless-stopped + networks: + - dev_network mailhog: image: mailhog/mailhog:latest @@ -29,6 +34,8 @@ services: - "1025:1025" # SMTP server - "8025:8025" # Web UI for email testing restart: unless-stopped + networks: + - dev_network redis: image: redis:latest @@ -37,6 +44,11 @@ services: volumes: - redis_data:/data restart: unless-stopped + networks: + - dev_network + +networks: + dev_network: volumes: psql_volume_bp: diff --git a/internal/app/container.go b/internal/app/container.go index 54120306..23990c9e 100644 --- a/internal/app/container.go +++ b/internal/app/container.go @@ -1,18 +1,19 @@ package app import ( - "sync" - "keizer-auth/internal/database" "keizer-auth/internal/repositories" "keizer-auth/internal/services" + "sync" ) type Container struct { - DB database.Service - AuthService *services.AuthService - SessionService *services.SessionService - EmailService *services.EmailService + DB database.Service + AuthService *services.AuthService + SessionService *services.SessionService + EmailService *services.EmailService + AccountService *services.AccountService + ApplicationService *services.ApplicationService } var ( @@ -27,14 +28,22 @@ func GetContainer() *Container { rds := database.NewRedisClient() userRepo := repositories.NewUserRepository(gormDB) + accountRepo := repositories.NewAccountRepository(gormDB) + applicationRepo := repositories.NewApplicationRepository(gormDB) + userAccountRepo := repositories.NewUserAccountRepository(gormDB) redisRepo := repositories.NewRedisRepository(rds) + authService := services.NewAuthService(userRepo, redisRepo) sessionService := services.NewSessionService(redisRepo, userRepo) + accountService := services.NewAccountService(accountRepo, userAccountRepo) + applicationService := services.NewApplicationService(applicationRepo, accountRepo) container = &Container{ - DB: db, - AuthService: authService, - SessionService: sessionService, + DB: db, + AuthService: authService, + SessionService: sessionService, + AccountService: accountService, + ApplicationService: applicationService, } }) return container diff --git a/internal/app/controllers.go b/internal/app/controllers.go index dfdaa24a..ce1eb109 100644 --- a/internal/app/controllers.go +++ b/internal/app/controllers.go @@ -3,11 +3,15 @@ package app import "keizer-auth/internal/controllers" type ServerControllers struct { - Auth *controllers.AuthController + Auth *controllers.AuthController + Account *controllers.AccountController + Application *controllers.ApplicationController } func GetControllers(container *Container) *ServerControllers { return &ServerControllers{ - Auth: controllers.NewAuthController(container.AuthService, container.SessionService), + Auth: controllers.NewAuthController(container.AuthService, container.SessionService), + Account: controllers.NewAccountController(container.AccountService), + Application: controllers.NewApplicationController(container.ApplicationService), } } diff --git a/internal/app/middlewares.go b/internal/app/middlewares.go new file mode 100644 index 00000000..a7ed7bb8 --- /dev/null +++ b/internal/app/middlewares.go @@ -0,0 +1,15 @@ +package app + +import ( + "keizer-auth/internal/middlewares" +) + +type ServerMiddlewares struct { + Auth *middlewares.AuthMiddleware +} + +func GetMiddlewares(container *Container) *ServerMiddlewares { + return &ServerMiddlewares{ + Auth: middlewares.NewAuthMiddleware(container.SessionService), + } +} diff --git a/internal/constants/constants.go b/internal/constants/constants.go new file mode 100644 index 00000000..f9e02bcb --- /dev/null +++ b/internal/constants/constants.go @@ -0,0 +1,3 @@ +package constants + +const UserContextKey = "currentUser" diff --git a/internal/controllers/account_controller.go b/internal/controllers/account_controller.go new file mode 100644 index 00000000..2ff141fa --- /dev/null +++ b/internal/controllers/account_controller.go @@ -0,0 +1,55 @@ +package controllers + +import ( + "keizer-auth/internal/models" + "keizer-auth/internal/services" + "keizer-auth/internal/utils" + "keizer-auth/internal/validators" + + "github.com/gofiber/fiber/v2" +) + +type AccountController struct { + accountService *services.AccountService +} + +func NewAccountController( + accountService *services.AccountService, +) *AccountController { + return &AccountController{accountService: accountService} +} + +func (self *AccountController) Get(c *fiber.Ctx) error { + user := utils.GetCurrentUser(c) + accounts, err := self.accountService.GetAccountsByUser(user.ID) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.JSON(accounts) +} + +func (self *AccountController) Create(c *fiber.Ctx) error { + var err error + user := utils.GetCurrentUser(c) + body := new(validators.CreateAccount) + + if err := c.BodyParser(body); err != nil { + return c. + Status(fiber.StatusBadRequest). + JSON(fiber.Map{"error": "Invalid request body"}) + } + + if err := body.ValidateFile(); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + account := new(models.Account) + account, err = self.accountService.Create(body.Name, user.ID) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + return c.JSON(account) +} diff --git a/internal/controllers/applicaton_controller.go b/internal/controllers/applicaton_controller.go new file mode 100644 index 00000000..39459401 --- /dev/null +++ b/internal/controllers/applicaton_controller.go @@ -0,0 +1,57 @@ +package controllers + +import ( + "keizer-auth/internal/models" + "keizer-auth/internal/services" + "keizer-auth/internal/utils" + "keizer-auth/internal/validators" + + "github.com/gofiber/fiber/v2" +) + +type ApplicationController struct { + applicationService *services.ApplicationService +} + +func NewApplicationController( + applicationService *services.ApplicationService, +) *ApplicationController { + return &ApplicationController{ + applicationService: applicationService, + } +} + +func (self *ApplicationController) Get(c *fiber.Ctx) error { + user := utils.GetCurrentUser(c) + applications, err := self.applicationService.Get(user.ID, user.ID) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.JSON(applications) +} + +func (self *ApplicationController) Create(c *fiber.Ctx) error { + var err error + user := utils.GetCurrentUser(c) + body := new(validators.CreateApplication) + + if err := c.BodyParser(body); err != nil { + return c. + Status(fiber.StatusBadRequest). + JSON(fiber.Map{"error": "Invalid request body"}) + } + + if err := body.ValidateFile(); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + account := new(models.Account) + account, err = self.applicationService.Create(body.Name, account.ID user.ID) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + return c.JSON(account) +} diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go index 8f5edf2e..14b79fea 100644 --- a/internal/controllers/auth_controller.go +++ b/internal/controllers/auth_controller.go @@ -3,7 +3,6 @@ package controllers import ( "errors" "fmt" - "keizer-auth/internal/models" "keizer-auth/internal/services" "keizer-auth/internal/utils" @@ -53,6 +52,9 @@ func (ac *AuthController) SignIn(c *fiber.Ctx) error { "error": "User is not verified. Please verify your account before signing in.", }) } + if user.Type != models.Dashboard { + return c.SendStatus(fiber.StatusBadRequest) + } isValid, err := ac.authService.VerifyPassword( body.Password, @@ -65,7 +67,7 @@ func (ac *AuthController) SignIn(c *fiber.Ctx) error { } if !isValid { return c. - Status(fiber.StatusUnauthorized). + Status(fiber.StatusBadRequest). JSON(fiber.Map{"error": "Invalid email or password. Please try again."}) } @@ -76,8 +78,10 @@ func (ac *AuthController) SignIn(c *fiber.Ctx) error { JSON(fiber.Map{"error": "Something went wrong, Failed to create session"}) } + fmt.Printf("%v", sessionId) + fmt.Print(sessionId) utils.SetSessionCookie(c, sessionId) - return c.JSON(fiber.Map{"message": "signed in successfully"}) + return c.JSON(user) } func (ac *AuthController) SignUp(c *fiber.Ctx) error { @@ -149,23 +153,10 @@ func (ac *AuthController) VerifyOTP(c *fiber.Ctx) error { } utils.SetSessionCookie(c, sessionID) - return c.JSON(fiber.Map{"message": "OTP Verified!"}) + return c.JSON(user) } -func (ac *AuthController) VerifyTokenHandler(c *fiber.Ctx) error { - sessionID := utils.GetSessionCookie(c) - if sessionID == "" { - return c. - Status(fiber.StatusUnauthorized). - JSON(fiber.Map{"error": "Unauthorized"}) - } - - user := new(models.User) - if err := ac.sessionService.GetSession(sessionID, user); err != nil { - return c. - Status(fiber.StatusUnauthorized). - JSON(fiber.Map{"error": "Unauthorized"}) - } - +func (ac *AuthController) Profile(c *fiber.Ctx) error { + user := utils.GetCurrentUser(c) return c.JSON(user) } diff --git a/internal/database/database.go b/internal/database/database.go index 0a1e0f8e..8e0028a8 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -3,13 +3,12 @@ package database import ( "context" "fmt" + "keizer-auth/internal/models" "log" "os" "strconv" "time" - "keizer-auth/internal/models" - _ "github.com/joho/godotenv/autoload" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -42,7 +41,6 @@ var ( ) func New() Service { - // Reuse Connection if dbInstance != nil { return dbInstance } @@ -83,10 +81,39 @@ func GetDB() *gorm.DB { } func autoMigrate(db *gorm.DB) error { - return db.AutoMigrate( - &models.User{}, + user := &models.User{} + if err := user.BeforeMigrate(db); err != nil { + return err + } + + if err := db.AutoMigrate( + user, &models.Domain{}, - ) + &models.Account{}, + &models.UserAccount{}, + ); err != nil { + return err + } + + if err := db.Exec(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.constraint_column_usage + WHERE table_name = 'user_accounts' + AND constraint_name = 'check_role' + ) THEN + ALTER TABLE user_accounts + ADD CONSTRAINT check_role + CHECK (role IN ('admin', 'member')); + END IF; + END $$; + `).Error; err != nil { + return err + } + + return nil } // Health checks the health of the database connection by pinging the database. diff --git a/internal/middlewares/auth_middleware.go b/internal/middlewares/auth_middleware.go new file mode 100644 index 00000000..c69a8637 --- /dev/null +++ b/internal/middlewares/auth_middleware.go @@ -0,0 +1,42 @@ +package middlewares + +import ( + "keizer-auth/internal/constants" + "keizer-auth/internal/models" + "keizer-auth/internal/services" + "keizer-auth/internal/utils" + "log" + + "github.com/gofiber/fiber/v2" +) + +type AuthMiddleware struct { + sessionService *services.SessionService +} + +func NewAuthMiddleware( + ss *services.SessionService, +) *AuthMiddleware { + return &AuthMiddleware{sessionService: ss} +} + +func (self *AuthMiddleware) Authorize(c *fiber.Ctx) error { + sessionID := utils.GetSessionCookie(c) + if sessionID == "" { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "unauthorized", + }) + } + + var user models.User + err := self.sessionService.GetSession(sessionID, &user) + if err != nil { + log.Printf("Session validation error: %v", err) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "unauthorized", + }) + } + + c.Locals(constants.UserContextKey, &user) + return c.Next() +} diff --git a/internal/models/account.go b/internal/models/account.go new file mode 100644 index 00000000..94a9af3f --- /dev/null +++ b/internal/models/account.go @@ -0,0 +1,48 @@ +package models + +import ( + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type UserAccountRole string + +const ( + RoleAdmin UserAccountRole = "admin" + RoleMember UserAccountRole = "member" +) + +type Account struct { + Name string `gorm:"not null;default:null;type:varchar(100)" json:"name"` + Logo string `gorm:"default:null" json:"logo"` + Base + Users []User `gorm:"many2many:user_accounts" json:"-"` +} + +type UserAccount struct { + UniqueConstraint string `gorm:"uniqueIndex:user_account_unique,priority:1" json:"-"` + Role UserAccountRole `gorm:"not null;type:varchar(50);default:'member'"` + Account Account `gorm:"foreignKey:AccountID"` + Base + User User `gorm:"foreignKey:UserID"` + AccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"account_id"` + UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"` +} + +func (self UserAccountRole) IsValid() bool { + switch self { + case RoleAdmin, RoleMember: + return true + default: + return false + } +} + +func (self *UserAccount) BeforeSave(tx *gorm.DB) error { + if !self.Role.IsValid() { + return fmt.Errorf("invalid role: %s", self.Role) + } + return nil +} diff --git a/internal/models/application.go b/internal/models/application.go new file mode 100644 index 00000000..3863f1d6 --- /dev/null +++ b/internal/models/application.go @@ -0,0 +1,27 @@ +package models + +import ( + "errors" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Application struct { + Name string `gorm:"not null;default:null;type:varchar(100)" json:"name"` + Logo string `gorm:"default:null" json:"logo"` + Base + Account Account `gorm:"foreignKey:AccountID"` + AccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"account_id"` +} + +func (a *Application) AfterCreate(tx *gorm.DB) error { + environments := []ApplicationEnvironment{ + {Name: "development", ApplicationID: a.ID}, + {Name: "production", ApplicationID: a.ID}, + } + if err := tx.Create(&environments).Error; err != nil { + return errors.New("Failed to create default environments") + } + return nil +} diff --git a/internal/models/application_auth_provider.go b/internal/models/application_auth_provider.go new file mode 100644 index 00000000..9d5a8c98 --- /dev/null +++ b/internal/models/application_auth_provider.go @@ -0,0 +1,29 @@ +package models + +import "github.com/google/uuid" + +type AuthProviderType string + +const ( + AuthProviderEmail AuthProviderType = "email" + // AuthProviderGoogle AuthProviderType = "google" + // AuthProviderGithub AuthProviderType = "github" + // AuthProviderMicrosoft AuthProviderType = "microsoft" +) + +type ApplicationAuthProvider struct { + Provider AuthProviderType `gorm:"type:varchar(50);not null" json:"provider"` + + // Specific configuration for each provider + ClientID string `gorm:"type:varchar(255)" json:"client_id,omitempty"` + ClientSecret string `gorm:"type:varchar(255)" json:"client_secret,omitempty"` + + Base + + // Additional provider-specific configurations can be added as needed + Scopes []string `gorm:"type:text[];serializer:json" json:"scopes,omitempty"` + + Application Application `gorm:"foreignKey:ApplicationID"` + ApplicationID uuid.UUID `gorm:"type:uuid;not null;index" json:"application_id"` + IsEnabled bool `gorm:"default:false" json:"is_enabled"` +} diff --git a/internal/models/application_environment.go b/internal/models/application_environment.go new file mode 100644 index 00000000..307a66a7 --- /dev/null +++ b/internal/models/application_environment.go @@ -0,0 +1,22 @@ +package models + +import ( + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ApplicationEnvironment struct { + Name string `gorm:"not null;type:varchar(50)" json:"name"` + Status string `gorm:"type:varchar(50);default:'active'" json:"status"` + Base + Application Application `gorm:"foreignKey:ApplicationID"` + ApplicationID uuid.UUID `gorm:"type:uuid;not null;index" json:"application_id"` + IsProtected bool `gorm:"not null;default:false" json:"is_protected"` +} + +func (e *ApplicationEnvironment) BeforeCreate(tx *gorm.DB) error { + if e.Name == "development" || e.Name == "production" { + e.IsProtected = true + } + return nil +} diff --git a/internal/models/main.go b/internal/models/main.go index bc297c16..4808484e 100644 --- a/internal/models/main.go +++ b/internal/models/main.go @@ -12,10 +12,10 @@ type Base struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt *time.Time `sql:"index" json:"deleted_at"` - ID uuid.UUID `gorm:"type:uuid"` + ID uuid.UUID `json:"id" gorm:"type:uuid"` } func (base *Base) BeforeCreate(tx *gorm.DB) (err error) { - base.ID = uuid.New() - return + base.ID, err = uuid.NewV7() + return err } diff --git a/internal/models/user.go b/internal/models/user.go index c9c5f19f..770512e0 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -1,6 +1,47 @@ package models -import "time" +import ( + "database/sql/driver" + "fmt" + "time" + + "gorm.io/gorm" +) + +type UserType string + +const ( + Dashboard UserType = "dashboard" + Member UserType = "member" +) + +func (self *UserType) Scan(value interface{}) error { + if value == "" { + *self = Member + return nil + } + + strVal, ok := value.(string) + if !ok { + return fmt.Errorf("Failed to convert") + } + + *self = UserType(strVal) + return nil +} + +func (self UserType) Value() (driver.Value, error) { + return string(self), nil +} + +func (ut UserType) Validate() bool { + switch ut { + case Dashboard, Member: + return true + default: + return false + } +} type User struct { LastLogin time.Time `json:"last_login"` @@ -8,7 +49,19 @@ type User struct { PasswordHash string `json:"-"` FirstName string `gorm:"not null;type:varchar(100);default:null" json:"first_name"` LastName string `gorm:"type:varchar(100);default:null" json:"last_name"` + Type UserType `gorm:"type:user_type;not null;default:'member'" json:"type"` Base IsVerified bool `gorm:"not null;default:false" json:"is_verified"` IsActive bool `gorm:"not null;default:false" json:"is_active"` } + +func (u *User) BeforeMigrate(db *gorm.DB) error { + return db.Exec(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_type') THEN + CREATE TYPE user_type AS ENUM ('dashboard', 'member'); + END IF; + END$$; + `).Error +} diff --git a/internal/repositories/account_repository.go b/internal/repositories/account_repository.go new file mode 100644 index 00000000..6c465442 --- /dev/null +++ b/internal/repositories/account_repository.go @@ -0,0 +1,68 @@ +package repositories + +import ( + "fmt" + "keizer-auth/internal/models" + + "github.com/google/uuid" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type AccountRepository struct { + db *gorm.DB +} + +func NewAccountRepository(db *gorm.DB) *AccountRepository { + return &AccountRepository{db: db} +} + +func (self *AccountRepository) Create(account *models.Account) error { + return self. + db.Model(account). + Clauses(clause.Returning{}). + Create(account). + Error +} + +func (self *AccountRepository) GetAccountsByUser( + userID uuid.UUID, +) (*[]models.Account, error) { + accounts := new([]models.Account) + if err := self.db. + Joins("JOIN user_accounts ON user_accounts.account_id = accounts.id"). + Where("user_accounts.user_id = ?", userID). + Find(accounts).Error; err != nil { + return nil, err + } + return accounts, nil +} + +func (self *AccountRepository) GetAccountByUser( + accountID uuid.UUID, + userID uuid.UUID, +) (*models.Account, error) { + account := new(models.Account) + if err := self. + db.Model(models.Account{}). + Joins("JOIN user_accounts ON user_accounts.account_id = accounts.id"). + Where("user_accounts.user_id = ? AND accounts.id = ?", userID, accountID). + First(account).Error; err != nil { + return nil, err + } + return account, nil +} + +func (self *AccountRepository) Get(uuid string) (*models.Account, error) { + account := new(models.Account) + result := self.db.First(&account, "id = ?", uuid) + + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("account not found") + } + return nil, fmt.Errorf("error in getting account: %w", result.Error) + } + + return account, nil +} diff --git a/internal/repositories/application_repository.go b/internal/repositories/application_repository.go new file mode 100644 index 00000000..a0c13a23 --- /dev/null +++ b/internal/repositories/application_repository.go @@ -0,0 +1,53 @@ +package repositories + +import ( + "fmt" + "keizer-auth/internal/models" + + "github.com/google/uuid" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type ApplicationRepository struct { + db *gorm.DB +} + +func NewApplicationRepository(db *gorm.DB) *ApplicationRepository { + return &ApplicationRepository{db: db} +} + +func (self *ApplicationRepository) Create(application *models.Application) error { + return self. + db.Model(application). + Clauses(clause.Returning{}). + Create(application). + Error +} + +func (self *ApplicationRepository) GetByID(uuid string) (*models.Application, error) { + application := new(models.Application) + result := self.db.First(&application, "id = ?", uuid) + + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("application not found") + } + return nil, fmt.Errorf("error in getting application: %w", result.Error) + } + + return application, nil +} + +func (self *ApplicationRepository) GetApplicationsByAccount( + accountID uuid.UUID, +) (*[]models.Application, error) { + applications := new([]models.Application) + err := self.db. + Where("account_id = ?", accountID). + Find(applications).Error + if err != nil { + return nil, err + } + return applications, nil +} diff --git a/internal/repositories/user_account_repository.go b/internal/repositories/user_account_repository.go new file mode 100644 index 00000000..08fb4f86 --- /dev/null +++ b/internal/repositories/user_account_repository.go @@ -0,0 +1,26 @@ +package repositories + +import ( + "keizer-auth/internal/models" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type UserAccountRepository struct { + db *gorm.DB +} + +func NewUserAccountRepository(db *gorm.DB) *UserAccountRepository { + return &UserAccountRepository{db: db} +} + +func (self *UserAccountRepository) Create( + userAccount *models.UserAccount, +) error { + return self. + db.Model(userAccount). + Clauses(clause.Returning{}). + Create(userAccount). + Error +} diff --git a/internal/repositories/user_repository.go b/internal/repositories/user_repository.go index e4f8e2a6..6149a893 100644 --- a/internal/repositories/user_repository.go +++ b/internal/repositories/user_repository.go @@ -2,7 +2,6 @@ package repositories import ( "fmt" - "keizer-auth/internal/models" "gorm.io/gorm" @@ -39,18 +38,6 @@ func (r *UserRepository) GetUser(uuid string) (*models.User, error) { return user, nil } -func (r *UserRepository) GetUserByEmail(user *models.User) error { - result := r.db.Where(user).First(user) - if result.Error != nil { - if result.Error == gorm.ErrRecordNotFound { - return nil - } - return fmt.Errorf("error in getting user: %w", result.Error) - } - - return nil -} - func (r *UserRepository) GetUserByStruct(user *models.User) error { result := r.db.Where(user).First(user) if result.Error != nil { diff --git a/internal/server/routes.go b/internal/server/routes.go index 8227a9d2..52824e99 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -25,7 +25,17 @@ func (s *FiberServer) RegisterFiberRoutes() { auth.Post("/sign-up", s.controllers.Auth.SignUp) auth.Post("/sign-in", s.controllers.Auth.SignIn) auth.Post("/verify-otp", s.controllers.Auth.VerifyOTP) - auth.Get("/verify-token", s.controllers.Auth.VerifyTokenHandler) + auth.Get("/profile", s.middlewars.Auth.Authorize, s.controllers.Auth.Profile) + + // accounts handlers + accounts := api.Group("/accounts", s.middlewars.Auth.Authorize) + accounts.Post("/", s.controllers.Account.Create) + accounts.Get("/", s.controllers.Account.Get) + + // applications handlers + applications := accounts.Group("/:accountId/applications") + applications.Post("/", s.controllers.Application.Create) + applications.Get("/", s.controllers.Application.Get) s.Static("/", "./web/dist") s.Static("*", "./web/dist/index.html") diff --git a/internal/server/server.go b/internal/server/server.go index 6981d6f8..c05382cf 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -10,11 +10,13 @@ type FiberServer struct { *fiber.App container *app.Container controllers *app.ServerControllers + middlewars *app.ServerMiddlewares } func New() *FiberServer { container := app.GetContainer() controllers := app.GetControllers(container) + middlewars := app.GetMiddlewares(container) server := &FiberServer{ App: fiber.New(fiber.Config{ @@ -23,6 +25,7 @@ func New() *FiberServer { }), container: container, controllers: controllers, + middlewars: middlewars, } return server diff --git a/internal/services/account_service.go b/internal/services/account_service.go new file mode 100644 index 00000000..2c3cd944 --- /dev/null +++ b/internal/services/account_service.go @@ -0,0 +1,52 @@ +package services + +import ( + "keizer-auth/internal/models" + "keizer-auth/internal/repositories" + + "github.com/google/uuid" +) + +type AccountService struct { + accountRepo *repositories.AccountRepository + userAccountRepo *repositories.UserAccountRepository +} + +func NewAccountService( + accountRepo *repositories.AccountRepository, + userAccountRepo *repositories.UserAccountRepository, +) *AccountService { + return &AccountService{accountRepo: accountRepo, userAccountRepo: userAccountRepo} +} + +func (self *AccountService) Create( + name string, + userID uuid.UUID, +) (*models.Account, error) { + account := models.Account{Name: name} + if err := self.accountRepo.Create(&account); err != nil { + return nil, err + } + + userAccount := models.UserAccount{ + UserID: userID, + AccountID: account.ID, + Role: "admin", + } + + if err := self.userAccountRepo.Create(&userAccount); err != nil { + return nil, err + } + + return &account, nil +} + +func (self *AccountService) GetAccountsByUser( + userID uuid.UUID, +) (*[]models.Account, error) { + accounts, err := self.accountRepo.GetAccountsByUser(userID) + if err != nil { + return nil, err + } + return accounts, nil +} diff --git a/internal/services/application_service.go b/internal/services/application_service.go new file mode 100644 index 00000000..c218c2f3 --- /dev/null +++ b/internal/services/application_service.go @@ -0,0 +1,65 @@ +package services + +import ( + "errors" + "keizer-auth/internal/models" + "keizer-auth/internal/repositories" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ApplicationService struct { + applicationRepo *repositories.ApplicationRepository + accountRepo *repositories.AccountRepository +} + +func NewApplicationService( + applicationRepo *repositories.ApplicationRepository, + accountRepo *repositories.AccountRepository, +) *ApplicationService { + return &ApplicationService{ + applicationRepo: applicationRepo, + accountRepo: accountRepo, + } +} + +func (self *ApplicationService) Create( + name string, + accountID uuid.UUID, + userID uuid.UUID, +) (*models.Application, error) { + _, err := self.accountRepo.GetAccountByUser(accountID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("account not found or unauthorized access") + } + return nil, err + } + + application := models.Application{Name: name, AccountID: accountID} + if err := self.applicationRepo.Create(&application); err != nil { + return nil, err + } + return &application, nil +} + +func (self *ApplicationService) Get( + accountID uuid.UUID, + userID uuid.UUID, +) (*[]models.Application, error) { + _, err := self.accountRepo.GetAccountByUser(accountID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("account not found or unauthorized access") + } + return nil, err + } + + applications, err := self.applicationRepo.GetApplicationsByAccount(accountID) + if err != nil { + return nil, err + } + + return applications, nil +} diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 9f04e664..faa0ce20 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -4,12 +4,11 @@ import ( "encoding/base64" "encoding/json" "fmt" - "time" - "keizer-auth/internal/models" "keizer-auth/internal/repositories" "keizer-auth/internal/utils" "keizer-auth/internal/validators" + "time" "github.com/nrednav/cuid2" "github.com/redis/go-redis/v9" @@ -20,7 +19,10 @@ type AuthService struct { redisRepo *repositories.RedisRepository } -func NewAuthService(userRepo *repositories.UserRepository, redisRepo *repositories.RedisRepository) *AuthService { +func NewAuthService( + userRepo *repositories.UserRepository, + redisRepo *repositories.RedisRepository, +) *AuthService { return &AuthService{userRepo: userRepo, redisRepo: redisRepo} } @@ -29,10 +31,9 @@ func (as *AuthService) RegisterUser( ) (string, error) { user := models.User{ Email: userRegister.Email, + Type: models.Dashboard, } - fmt.Print(user) - fmt.Print(user.IsVerified) err := as.userRepo.GetUserByStruct(&user) if err != nil { return "", err @@ -96,7 +97,9 @@ func (as *AuthService) VerifyPassword( return utils.VerifyPassword(password, passwordHash) } -func (as *AuthService) VerifyOTP(verifyOtpBody *validators.VerifyOTP) (string, bool, error) { +func (as *AuthService) VerifyOTP( + verifyOtpBody *validators.VerifyOTP, +) (string, bool, error) { encodedOtpData, err := as.redisRepo.Get(verifyOtpBody.Id) if err != nil { if err == redis.Nil { diff --git a/internal/services/session_service.go b/internal/services/session_service.go index 56221722..8ebd134a 100644 --- a/internal/services/session_service.go +++ b/internal/services/session_service.go @@ -3,11 +3,10 @@ package services import ( "encoding/json" "fmt" - "time" - "keizer-auth/internal/models" "keizer-auth/internal/repositories" "keizer-auth/internal/utils" + "time" "github.com/redis/go-redis/v9" ) @@ -17,27 +16,26 @@ type SessionService struct { userRepo *repositories.UserRepository } -func NewSessionService(redisRepo *repositories.RedisRepository, userRepo *repositories.UserRepository) *SessionService { +func NewSessionService( + redisRepo *repositories.RedisRepository, + userRepo *repositories.UserRepository, +) *SessionService { return &SessionService{redisRepo: redisRepo, userRepo: userRepo} } func (ss *SessionService) CreateSession(user *models.User) (string, error) { - sessionID, err := utils.GenerateSessionID() - if err != nil { - return "", fmt.Errorf("error in generating session %w", err) - } + sessionID := utils.GenerateSessionID() userJson, err := json.Marshal(user) if err != nil { return "", fmt.Errorf("error occured %w", err) } - err = ss.redisRepo.Set( + if err = ss.redisRepo.Set( "dashboard-user-session-"+sessionID, string(userJson), utils.SessionExpiresIn, - ) - if err != nil { + ); err != nil { return "", fmt.Errorf("error in setting session %w", err) } diff --git a/internal/utils/session_helpers.go b/internal/utils/session_helpers.go index 4f33dc31..e95331dc 100644 --- a/internal/utils/session_helpers.go +++ b/internal/utils/session_helpers.go @@ -1,7 +1,8 @@ package utils import ( - "fmt" + "keizer-auth/internal/constants" + "keizer-auth/internal/models" "time" "github.com/gofiber/fiber/v2" @@ -10,16 +11,8 @@ import ( const SessionExpiresIn = 30 * 24 * time.Hour -func GenerateSessionID() (string, error) { - generate, err := cuid2.Init( - cuid2.WithLength(15), - ) - if err != nil { - fmt.Println(err.Error()) - return "", err - } - - return generate(), nil +func GenerateSessionID() string { + return cuid2.Generate() } func SetSessionCookie(c *fiber.Ctx, sessionID string) { @@ -28,14 +21,19 @@ func SetSessionCookie(c *fiber.Ctx, sessionID string) { Value: sessionID, Expires: time.Now().Add(SessionExpiresIn), HTTPOnly: true, - Secure: true, + Secure: false, SameSite: fiber.CookieSameSiteNoneMode, - // TODO: handle domain - Domain: "localhost", - Path: "/", }) } func GetSessionCookie(c *fiber.Ctx) string { return c.Cookies("session_id", "") } + +func GetCurrentUser(c *fiber.Ctx) *models.User { + user, ok := c.Locals(constants.UserContextKey).(*models.User) + if !ok { + return nil + } + return user +} diff --git a/internal/validators/account.go b/internal/validators/account.go new file mode 100644 index 00000000..5f1d2d74 --- /dev/null +++ b/internal/validators/account.go @@ -0,0 +1,31 @@ +package validators + +import ( + "errors" + "mime/multipart" +) + +type CreateAccount struct { + Logo *multipart.FileHeader `json:"-" form:"logo"` + Name string `validate:"required|maxLen:100" form:"name" json:"name" label:"Account Name"` +} + +func (self CreateAccount) ValidateFile() error { + if self.Logo == nil { + return nil + } + + const maxFileSize = 2 * 1024 * 1024 + if self.Logo.Size > maxFileSize { + return errors.New("file size must not exceed 2 MB") + } + + validTypes := []string{"image/png", "image/jpeg"} + for _, t := range validTypes { + if self.Logo.Header.Get("Content-Type") == t { + return nil + } + } + + return errors.New("file must be a PNG or JPEG image") +} diff --git a/internal/validators/application.go b/internal/validators/application.go new file mode 100644 index 00000000..1271fa99 --- /dev/null +++ b/internal/validators/application.go @@ -0,0 +1,31 @@ +package validators + +import ( + "errors" + "mime/multipart" +) + +type CreateApplication struct { + Logo *multipart.FileHeader `json:"-" form:"logo"` + Name string `validate:"required|maxLen:100" form:"name" json:"name" label:"Application Name"` +} + +func (self CreateApplication) ValidateFile() error { + if self.Logo == nil { + return nil + } + + const maxFileSize = 2 * 1024 * 1024 + if self.Logo.Size > maxFileSize { + return errors.New("file size must not exceed 2 MB") + } + + validTypes := []string{"image/png", "image/jpeg"} + for _, t := range validTypes { + if self.Logo.Header.Get("Content-Type") == t { + return nil + } + } + + return errors.New("file must be a PNG or JPEG image") +} diff --git a/internal/validators/sign_up.go b/internal/validators/auth.go similarity index 84% rename from internal/validators/sign_up.go rename to internal/validators/auth.go index c7f0c174..d148d157 100644 --- a/internal/validators/sign_up.go +++ b/internal/validators/auth.go @@ -1,9 +1,8 @@ package validators import ( - "unicode" - "keizer-auth/internal/utils" + "unicode" "github.com/gookit/validate" ) @@ -38,7 +37,7 @@ type SignUpUser struct { LastName string `json:"last_name" validate:"maxLen:32" label:"Last Name"` } -func (f SignUpUser) Messages() map[string]string { +func (f *SignUpUser) Messages() map[string]string { return validate.MS{ // Global messages "required": "{field} is required", @@ -67,3 +66,13 @@ func (u *SignUpUser) Validate() (bool, map[string]map[string]string) { return true, nil } + +type SignInUser struct { + Email string `validate:"required|email" label:"Email"` + Password string `validate:"required|minLen:8|password" label:"Password"` +} + +type VerifyOTP struct { + Otp string `validate:"required" label:"OTP"` + Id string `validate:"required" label:"Id"` +} diff --git a/internal/validators/sign_in.go b/internal/validators/sign_in.go deleted file mode 100644 index dd7ae5fb..00000000 --- a/internal/validators/sign_in.go +++ /dev/null @@ -1,6 +0,0 @@ -package validators - -type SignInUser struct { - Email string `validate:"required|email" label:"Email"` - Password string `validate:"required|minLen:8|password" label:"Password"` -} diff --git a/internal/validators/verify_otp.go b/internal/validators/verify_otp.go deleted file mode 100644 index ad243276..00000000 --- a/internal/validators/verify_otp.go +++ /dev/null @@ -1,6 +0,0 @@ -package validators - -type VerifyOTP struct { - Otp string `validate:"required" label:"OTP"` - Id string `validate:"required" label:"Id"` -} diff --git a/web/package.json b/web/package.json index 022ba662..e40c1894 100644 --- a/web/package.json +++ b/web/package.json @@ -13,7 +13,9 @@ "@hookform/resolvers": "^3.9.1", "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.1", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-separator": "^1.1.0", @@ -26,6 +28,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "input-otp": "^1.4.1", + "js-cookie": "^3.0.5", "lucide-react": "^0.454.0", "next-themes": "^0.4.3", "react": "^18.3.1", @@ -34,6 +37,7 @@ "sonner": "^1.7.0", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.1", "zod": "^3.23.8", "zustand": "^5.0.1" }, @@ -41,6 +45,7 @@ "@eslint/js": "^9.14.0", "@tanstack/eslint-plugin-query": "^5.59.7", "@tanstack/router-plugin": "^1.79.0", + "@types/js-cookie": "^3.0.6", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react-swc": "^3.5.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index d96083b1..733a7542 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -10,40 +10,46 @@ importers: dependencies: '@hookform/resolvers': specifier: ^3.9.1 - version: 3.9.1(react-hook-form@7.53.1) + version: 3.9.1(react-hook-form@7.53.1(react@18.3.1)) '@radix-ui/react-alert-dialog': specifier: ^1.1.2 - version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-avatar': specifier: ^1.1.1 - version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collapsible': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.1.2 - version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: ^1.3.1 version: 1.3.1(react@18.3.1) '@radix-ui/react-label': specifier: ^2.1.0 - version: 2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + version: 2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-separator': specifier: ^1.1.0 - version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.1.3 - version: 1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-query': specifier: ^5.59.19 version: 5.59.20(react@18.3.1) '@tanstack/react-router': specifier: ^1.79.0 - version: 1.79.0(react-dom@18.3.1)(react@18.3.1) + version: 1.79.0(@tanstack/router-generator@1.79.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-devtools': specifier: ^1.79.0 - version: 1.79.0(@tanstack/react-router@1.79.0)(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1) + version: 1.79.0(@tanstack/react-router@1.79.0(@tanstack/router-generator@1.79.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) axios: specifier: ^1.7.7 version: 1.7.7 @@ -55,13 +61,16 @@ importers: version: 2.1.1 input-otp: specifier: ^1.4.1 - version: 1.4.1(react-dom@18.3.1)(react@18.3.1) + version: 1.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 lucide-react: specifier: ^0.454.0 version: 0.454.0(react@18.3.1) next-themes: specifier: ^0.4.3 - version: 0.4.3(react-dom@18.3.1)(react@18.3.1) + version: 0.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -73,29 +82,35 @@ importers: version: 7.53.1(react@18.3.1) sonner: specifier: ^1.7.0 - version: 1.7.0(react-dom@18.3.1)(react@18.3.1) + version: 1.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^2.5.4 version: 2.5.4 tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.14) + vaul: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) zod: specifier: ^3.23.8 version: 3.23.8 zustand: specifier: ^5.0.1 - version: 5.0.1(@types/react@18.3.12)(react@18.3.1) + version: 5.0.1(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)) devDependencies: '@eslint/js': specifier: ^9.14.0 version: 9.14.0 '@tanstack/eslint-plugin-query': specifier: ^5.59.7 - version: 5.59.20(eslint@9.14.0)(typescript@5.6.3) + version: 5.59.20(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) '@tanstack/router-plugin': specifier: ^1.79.0 version: 1.79.0(vite@5.4.10) + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@types/react': specifier: ^18.3.12 version: 18.3.12 @@ -110,22 +125,22 @@ importers: version: 10.4.20(postcss@8.4.47) eslint: specifier: ^9.13.0 - version: 9.14.0 + version: 9.14.0(jiti@1.21.6) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@9.14.0) + version: 9.1.0(eslint@9.14.0(jiti@1.21.6)) eslint-plugin-react-hooks: specifier: ^5.0.0 - version: 5.0.0(eslint@9.14.0) + version: 5.0.0(eslint@9.14.0(jiti@1.21.6)) eslint-plugin-react-refresh: specifier: ^0.4.14 - version: 0.4.14(eslint@9.14.0) + version: 0.4.14(eslint@9.14.0(jiti@1.21.6)) eslint-plugin-simple-import-sort: specifier: ^12.1.1 - version: 12.1.1(eslint@9.14.0) + version: 12.1.1(eslint@9.14.0(jiti@1.21.6)) eslint-plugin-unused-imports: specifier: ^4.1.4 - version: 4.1.4(eslint@9.14.0) + version: 4.1.4(@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6)) globals: specifier: ^15.11.0 version: 15.12.0 @@ -146,7 +161,7 @@ importers: version: 5.6.3 typescript-eslint: specifier: ^8.13.0 - version: 8.13.0(eslint@9.14.0)(typescript@5.6.3) + version: 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) vite: specifier: ^5.4.10 version: 5.4.10 @@ -676,6 +691,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.1': + resolution: {integrity: sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==} + 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 + + '@radix-ui/react-collection@1.1.0': + resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} + 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 + '@radix-ui/react-compose-refs@1.1.0': resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} peerDependencies: @@ -716,6 +757,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.1.1': resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==} peerDependencies: @@ -729,6 +779,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.2': + resolution: {integrity: sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==} + 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 + '@radix-ui/react-focus-guards@1.1.1': resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: @@ -778,6 +841,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-menu@2.1.2': + resolution: {integrity: sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==} + 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 + '@radix-ui/react-popper@1.2.0': resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} peerDependencies: @@ -830,6 +906,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.0': + resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} + 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 + '@radix-ui/react-separator@1.1.0': resolution: {integrity: sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==} peerDependencies: @@ -1183,6 +1272,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1701,6 +1793,10 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2253,6 +2349,12 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vaul@1.1.1: + resolution: {integrity: sha512-+ejzF6ffQKPcfgS7uOrGn017g39F8SO4yLPXbBhpC7a0H+oPqPna8f1BUfXaz8eU4+pxbQcmjxW+jWBSbxjaFg==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 + vite@5.4.10: resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2597,9 +2699,9 @@ snapshots: '@esbuild/win32-x64@0.23.1': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0(jiti@1.21.6))': dependencies: - eslint: 9.14.0 + eslint: 9.14.0(jiti@1.21.6) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -2645,7 +2747,7 @@ snapshots: '@floating-ui/core': 1.6.8 '@floating-ui/utils': 0.2.8 - '@floating-ui/react-dom@2.1.2(react-dom@18.3.1)(react@18.3.1)': + '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/dom': 1.6.12 react: 18.3.1 @@ -2653,7 +2755,7 @@ snapshots: '@floating-ui/utils@0.2.8': {} - '@hookform/resolvers@3.9.1(react-hook-form@7.53.1)': + '@hookform/resolvers@3.9.1(react-hook-form@7.53.1(react@18.3.1))': dependencies: react-hook-form: 7.53.1(react@18.3.1) @@ -2713,100 +2815,159 @@ snapshots: '@radix-ui/primitive@1.1.0': {} - '@radix-ui/react-alert-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-alert-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + + '@radix-ui/react-avatar@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-avatar@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-collapsible@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 '@radix-ui/react-context@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 '@radix-ui/react-context@1.1.1(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 - '@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-direction@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + + '@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-menu': 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 - '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 '@radix-ui/react-icons@1.3.1(react@18.3.1)': dependencies: @@ -2815,134 +2976,193 @@ snapshots: '@radix-ui/react-id@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 - '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + + '@radix-ui/react-menu@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/rect': 1.1.0 - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-separator@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + + '@radix-ui/react-separator@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 '@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 - '@radix-ui/react-tooltip@1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-tooltip@1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/rect': 1.1.0 - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 '@radix-ui/react-use-size@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@types/react': 18.3.12 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 - '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 '@radix-ui/rect@1.1.0': {} @@ -3052,10 +3272,10 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/eslint-plugin-query@5.59.20(eslint@9.14.0)(typescript@5.6.3)': + '@tanstack/eslint-plugin-query@5.59.20(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': dependencies: - '@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3) - eslint: 9.14.0 + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + eslint: 9.14.0(jiti@1.21.6) transitivePeerDependencies: - supports-color - typescript @@ -3069,25 +3289,27 @@ snapshots: '@tanstack/query-core': 5.59.20 react: 18.3.1 - '@tanstack/react-router@1.79.0(react-dom@18.3.1)(react@18.3.1)': + '@tanstack/react-router@1.79.0(@tanstack/router-generator@1.79.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/history': 1.61.1 - '@tanstack/react-store': 0.5.6(react-dom@18.3.1)(react@18.3.1) + '@tanstack/react-store': 0.5.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 + optionalDependencies: + '@tanstack/router-generator': 1.79.0 - '@tanstack/react-store@0.5.6(react-dom@18.3.1)(react@18.3.1)': + '@tanstack/react-store@0.5.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/store': 0.5.5 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) use-sync-external-store: 1.2.2(react@18.3.1) - '@tanstack/router-devtools@1.79.0(@tanstack/react-router@1.79.0)(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1)': + '@tanstack/router-devtools@1.79.0(@tanstack/react-router@1.79.0(@tanstack/router-generator@1.79.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/react-router': 1.79.0(react-dom@18.3.1)(react@18.3.1) + '@tanstack/react-router': 1.79.0(@tanstack/router-generator@1.79.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 2.1.1 goober: 2.1.16(csstype@3.1.3) react: 18.3.1 @@ -3121,8 +3343,9 @@ snapshots: babel-dead-code-elimination: 1.0.6 chokidar: 3.6.0 unplugin: 1.15.0 - vite: 5.4.10 zod: 3.23.8 + optionalDependencies: + vite: 5.4.10 transitivePeerDependencies: - supports-color - webpack-sources @@ -3154,6 +3377,8 @@ snapshots: '@types/estree@1.0.6': {} + '@types/js-cookie@3.0.6': {} + '@types/json-schema@7.0.15': {} '@types/prop-types@15.7.13': {} @@ -3167,31 +3392,33 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 - '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0)(eslint@9.14.0)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.13.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) '@typescript-eslint/scope-manager': 8.13.0 - '@typescript-eslint/type-utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3) - '@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/type-utils': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.13.0 - eslint: 9.14.0 + eslint: 9.14.0(jiti@1.21.6) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3)': + '@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': dependencies: '@typescript-eslint/scope-manager': 8.13.0 '@typescript-eslint/types': 8.13.0 '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.13.0 debug: 4.3.7 - eslint: 9.14.0 + eslint: 9.14.0(jiti@1.21.6) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -3201,12 +3428,13 @@ snapshots: '@typescript-eslint/types': 8.13.0 '@typescript-eslint/visitor-keys': 8.13.0 - '@typescript-eslint/type-utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)': + '@typescript-eslint/type-utils@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': dependencies: '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) - '@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) debug: 4.3.7 ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - eslint @@ -3224,17 +3452,18 @@ snapshots: minimatch: 9.0.5 semver: 7.6.3 ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)': + '@typescript-eslint/utils@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@1.21.6)) '@typescript-eslint/scope-manager': 8.13.0 '@typescript-eslint/types': 8.13.0 '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) - eslint: 9.14.0 + eslint: 9.14.0(jiti@1.21.6) transitivePeerDependencies: - supports-color - typescript @@ -3478,25 +3707,27 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@9.1.0(eslint@9.14.0): + eslint-config-prettier@9.1.0(eslint@9.14.0(jiti@1.21.6)): dependencies: - eslint: 9.14.0 + eslint: 9.14.0(jiti@1.21.6) - eslint-plugin-react-hooks@5.0.0(eslint@9.14.0): + eslint-plugin-react-hooks@5.0.0(eslint@9.14.0(jiti@1.21.6)): dependencies: - eslint: 9.14.0 + eslint: 9.14.0(jiti@1.21.6) - eslint-plugin-react-refresh@0.4.14(eslint@9.14.0): + eslint-plugin-react-refresh@0.4.14(eslint@9.14.0(jiti@1.21.6)): dependencies: - eslint: 9.14.0 + eslint: 9.14.0(jiti@1.21.6) - eslint-plugin-simple-import-sort@12.1.1(eslint@9.14.0): + eslint-plugin-simple-import-sort@12.1.1(eslint@9.14.0(jiti@1.21.6)): dependencies: - eslint: 9.14.0 + eslint: 9.14.0(jiti@1.21.6) - eslint-plugin-unused-imports@4.1.4(eslint@9.14.0): + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6)): dependencies: - eslint: 9.14.0 + eslint: 9.14.0(jiti@1.21.6) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) eslint-scope@8.2.0: dependencies: @@ -3507,9 +3738,9 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.14.0: + eslint@9.14.0(jiti@1.21.6): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@1.21.6)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.18.0 '@eslint/core': 0.7.0 @@ -3544,6 +3775,8 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 text-table: 0.2.0 + optionalDependencies: + jiti: 1.21.6 transitivePeerDependencies: - supports-color @@ -3675,7 +3908,7 @@ snapshots: imurmurhash@0.1.4: {} - input-otp@1.4.1(react-dom@18.3.1)(react@18.3.1): + input-otp@1.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -3712,6 +3945,8 @@ snapshots: jiti@1.21.6: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -3798,7 +4033,7 @@ snapshots: natural-compare@1.4.0: {} - next-themes@0.4.3(react-dom@18.3.1)(react@18.3.1): + next-themes@0.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -3870,8 +4105,9 @@ snapshots: postcss-load-config@4.0.2(postcss@8.4.47): dependencies: lilconfig: 3.1.2 - postcss: 8.4.47 yaml: 2.6.0 + optionalDependencies: + postcss: 8.4.47 postcss-nested@6.2.0(postcss@8.4.47): dependencies: @@ -3917,28 +4153,31 @@ snapshots: react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1): dependencies: - '@types/react': 18.3.12 react: 18.3.1 react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 react-remove-scroll@2.6.0(@types/react@18.3.12)(react@18.3.1): dependencies: - '@types/react': 18.3.12 react: 18.3.1 react-remove-scroll-bar: 2.3.6(@types/react@18.3.12)(react@18.3.1) react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) tslib: 2.8.1 use-callback-ref: 1.3.2(@types/react@18.3.12)(react@18.3.1) use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 react-style-singleton@2.2.1(@types/react@18.3.12)(react@18.3.1): dependencies: - '@types/react': 18.3.12 get-nonce: 1.0.1 invariant: 2.2.4 react: 18.3.1 tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 react@18.3.1: dependencies: @@ -4008,7 +4247,7 @@ snapshots: signal-exit@4.1.0: {} - sonner@1.7.0(react-dom@18.3.1)(react@18.3.1): + sonner@1.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -4123,11 +4362,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.13.0(eslint@9.14.0)(typescript@5.6.3): + typescript-eslint@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.13.0(@typescript-eslint/parser@8.13.0)(eslint@9.14.0)(typescript@5.6.3) - '@typescript-eslint/parser': 8.13.0(eslint@9.14.0)(typescript@5.6.3) - '@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/eslint-plugin': 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - eslint @@ -4152,16 +4392,18 @@ snapshots: use-callback-ref@1.3.2(@types/react@18.3.12)(react@18.3.1): dependencies: - '@types/react': 18.3.12 react: 18.3.1 tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 use-sidecar@1.1.2(@types/react@18.3.12)(react@18.3.1): dependencies: - '@types/react': 18.3.12 detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 use-sync-external-store@1.2.2(react@18.3.1): dependencies: @@ -4169,6 +4411,15 @@ snapshots: util-deprecate@1.0.2: {} + vaul@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vite@5.4.10: dependencies: esbuild: 0.21.5 @@ -4205,7 +4456,8 @@ snapshots: zod@3.23.8: {} - zustand@5.0.1(@types/react@18.3.12)(react@18.3.1): - dependencies: + zustand@5.0.1(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)): + optionalDependencies: '@types/react': 18.3.12 react: 18.3.1 + use-sync-external-store: 1.2.2(react@18.3.1) diff --git a/web/src/actions/accounts/index.ts b/web/src/actions/accounts/index.ts new file mode 100644 index 00000000..a2cb5015 --- /dev/null +++ b/web/src/actions/accounts/index.ts @@ -0,0 +1,10 @@ +import apiClient from "~/axios"; +import { setAccounts } from "~/global-state/accounts"; +import type { AccountInterface } from "~/schema/account"; + +export const getAccounts = async () => { + return apiClient.get("accounts").then((r) => { + setAccounts(r.data); + return r.data; + }); +}; diff --git a/web/src/actions/accounts/mutations.ts b/web/src/actions/accounts/mutations.ts new file mode 100644 index 00000000..8126cd3d --- /dev/null +++ b/web/src/actions/accounts/mutations.ts @@ -0,0 +1,12 @@ +import apiClient from "~/axios"; +import { AccountInterface, CreateAccountInterface } from "~/schema/account"; + +export const createAccountFn = async (values: CreateAccountInterface) => { + const formData = new FormData(); + formData.append("name", values.name); + if (values.logo) formData.append("logo", values.logo); + + return apiClient + .post("/accounts", formData) + .then((r) => r.data); +}; diff --git a/web/src/actions/accounts/query-options.ts b/web/src/actions/accounts/query-options.ts new file mode 100644 index 00000000..d1714941 --- /dev/null +++ b/web/src/actions/accounts/query-options.ts @@ -0,0 +1,8 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { getAccounts } from "./index"; + +export const getAccountQueryOption = queryOptions({ + queryKey: ["get-accounts"], + queryFn: getAccounts, +}); diff --git a/web/src/actions/auth/profile.ts b/web/src/actions/auth/profile.ts new file mode 100644 index 00000000..8cf29c69 --- /dev/null +++ b/web/src/actions/auth/profile.ts @@ -0,0 +1,5 @@ +import apiClient from "~/axios"; +import { UserInterface } from "~/schema/user"; + +export const profile = async () => + await apiClient.get("auth/profile").then((res) => res.data); diff --git a/web/src/actions/auth/sign-in.ts b/web/src/actions/auth/sign-in.ts index 31a8a57b..a2f31915 100644 --- a/web/src/actions/auth/sign-in.ts +++ b/web/src/actions/auth/sign-in.ts @@ -2,12 +2,11 @@ import { z } from "zod"; import apiClient from "~/axios"; import type { emailPassSignInSchema } from "~/schema/auth"; - -interface SignInRes { - message: string; -} +import { UserInterface } from "~/schema/user"; export const signInMutationFn = async ( data: z.infer, ) => - await apiClient.post("auth/sign-in", data).then((res) => res.data); + await apiClient + .post("auth/sign-in", data) + .then((res) => res.data); diff --git a/web/src/actions/auth/verify-otp.ts b/web/src/actions/auth/verify-otp.ts index 5536c405..adbc7452 100644 --- a/web/src/actions/auth/verify-otp.ts +++ b/web/src/actions/auth/verify-otp.ts @@ -2,15 +2,12 @@ import { z } from "zod"; import apiClient from "~/axios"; import { verifyOtpSchema } from "~/schema/auth"; +import { UserInterface } from "~/schema/user"; export type VerifyOtpInterface = z.infer; -interface VerifyOtpRes { - message: string; -} - export async function verifyOtpMutationFn(values: VerifyOtpInterface) { return apiClient - .post("auth/verify-otp", values) + .post("auth/verify-otp", values) .then((r) => r.data); } diff --git a/web/src/actions/auth/verify-token.ts b/web/src/actions/auth/verify-token.ts deleted file mode 100644 index e93c6aaf..00000000 --- a/web/src/actions/auth/verify-token.ts +++ /dev/null @@ -1,7 +0,0 @@ -import apiClient from "~/axios"; - -export const verifyTokenQueryFn = async () => - await apiClient - .get("auth/verify-token") - .then((res) => console.log(res.data)) - .catch(console.log); diff --git a/web/src/axios.ts b/web/src/axios.ts index a69f3ede..40515c25 100644 --- a/web/src/axios.ts +++ b/web/src/axios.ts @@ -2,6 +2,7 @@ import axios from "axios"; const apiClient = axios.create({ baseURL: import.meta.env.VITE_BACKEND_URL, + withCredentials: true, }); apiClient.interceptors.request.use((config) => { @@ -10,9 +11,6 @@ apiClient.interceptors.request.use((config) => { } else { config.headers["Content-Type"] = "application/json"; } - - config.withCredentials = true; - config.headers.Accept = "application/json"; return config; }); diff --git a/web/src/components/account/create.tsx b/web/src/components/account/create.tsx new file mode 100644 index 00000000..db22dc5d --- /dev/null +++ b/web/src/components/account/create.tsx @@ -0,0 +1,82 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { Dispatch, SetStateAction } from "react"; +import { useForm } from "react-hook-form"; + +import { createAccountFn } from "~/actions/accounts/mutations"; +import useAccountStore from "~/global-state/accounts"; +import { CreateAccountInterface, createAccountSchema } from "~/schema/account"; + +import { Button } from "../ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { Form, FormField } from "../ui/form"; +import { Input } from "../ui/input"; + +interface Props { + open: boolean; + setOpen: Dispatch>; +} + +export const CreateAccount = ({ open, setOpen }: Props) => { + const { data: accounts, setData: setAccounts } = useAccountStore(); + + const form = useForm({ + resolver: zodResolver(createAccountSchema), + }); + + const { mutate, isPending } = useMutation({ + mutationFn: createAccountFn, + onSuccess: (data) => { + setAccounts([...accounts.filter((a) => a.id !== data.id), data]); + setOpen(false); + }, + }); + + function onOpenChange(value: boolean) { + if (!value && isPending) return; + return setOpen(value); + } + + function onSubmit(values: CreateAccountInterface) { + mutate(values); + } + + return ( + + + + Create Account + + +
+ + ( + + )} + /> + + + + + + +
+
+ ); +}; diff --git a/web/src/components/account/switcher.tsx b/web/src/components/account/switcher.tsx new file mode 100644 index 00000000..d06c82de --- /dev/null +++ b/web/src/components/account/switcher.tsx @@ -0,0 +1,97 @@ +import { ChevronsUpDown, GalleryVerticalEnd, Plus } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "~/components/ui/sidebar"; +import useAccountStore from "~/global-state/accounts"; +import useActiveAccountStore from "~/global-state/persistant-storage/selected-account"; + +import { buttonVariants } from "../ui/button"; +import { CreateAccount } from "./create"; + +export function AccountSwitcher() { + const { isMobile } = useSidebar(); + const accounts = useAccountStore.use.data(); + const { data: activeAccountId, setData: setActiveAccount } = + useActiveAccountStore(); + + const [openCreateAccount, setOpenCreateAccount] = useState(false); + + const activeAccount = useMemo(() => { + if (!activeAccountId) return null; + return accounts.find((a) => a.id === activeAccountId); + }, [accounts, activeAccountId]); + + useEffect(() => { + if (accounts && accounts.length > 0 && !activeAccountId) { + setActiveAccount(accounts[0].id); + } + }, [accounts, activeAccountId, setActiveAccount]); + + return ( + + + + + + + + + {activeAccount?.name ?? "-"} + + + + + + Accounts + + + {accounts.map((account) => ( + setActiveAccount(account.id)} + className="gap-2 p-2" + > + {account.name} + + ))} + + setOpenCreateAccount(true)} + className="gap-2 p-2" + > +
+ +
+
Add team
+
+
+
+
+
+ ); +} diff --git a/web/src/components/app-sidebar/index.tsx b/web/src/components/app-sidebar/index.tsx new file mode 100644 index 00000000..54373362 --- /dev/null +++ b/web/src/components/app-sidebar/index.tsx @@ -0,0 +1,180 @@ +import { useQuery } from "@tanstack/react-query"; +import { + BookOpen, + Bot, + Frame, + LifeBuoy, + Map, + PieChart, + Send, + Settings2, + SquareTerminal, +} from "lucide-react"; +import * as React from "react"; +import { useEffect } from "react"; + +import { getAccountQueryOption } from "~/actions/accounts/query-options"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + useSidebar, +} from "~/components/ui/sidebar"; + +import { AccountSwitcher } from "../account/switcher"; +import { NavMain } from "./nav-main"; +import { NavUser } from "./nav-user"; + +const data = { + user: { + name: "shadcn", + email: "m@example.com", + avatar: "/avatars/shadcn.jpg", + }, + navMain: [ + { + title: "Playground", + url: "#", + icon: SquareTerminal, + isActive: true, + items: [ + { + title: "History", + url: "#", + }, + { + title: "Starred", + url: "#", + }, + { + title: "Settings", + url: "#", + }, + ], + }, + { + title: "Models", + url: "#", + icon: Bot, + items: [ + { + title: "Genesis", + url: "#", + }, + { + title: "Explorer", + url: "#", + }, + { + title: "Quantum", + url: "#", + }, + ], + }, + { + title: "Documentation", + url: "#", + icon: BookOpen, + items: [ + { + title: "Introduction", + url: "#", + }, + { + title: "Get Started", + url: "#", + }, + { + title: "Tutorials", + url: "#", + }, + { + title: "Changelog", + url: "#", + }, + ], + }, + { + title: "Settings", + url: "#", + icon: Settings2, + items: [ + { + title: "General", + url: "#", + }, + { + title: "Team", + url: "#", + }, + { + title: "Billing", + url: "#", + }, + { + title: "Limits", + url: "#", + }, + ], + }, + ], + navSecondary: [ + { + title: "Support", + url: "#", + icon: LifeBuoy, + }, + { + title: "Feedback", + url: "#", + icon: Send, + }, + ], + projects: [ + { + name: "Design Engineering", + url: "#", + icon: Frame, + }, + { + name: "Sales & Marketing", + url: "#", + icon: PieChart, + }, + { + name: "Travel", + url: "#", + icon: Map, + }, + ], +}; + +export function AppSidebar({ ...props }: React.ComponentProps) { + const { setOpenMobile } = useSidebar(); + const { data: accounts, isPending } = useQuery(getAccountQueryOption); + + useEffect(() => { + if (accounts && accounts.length === 0) setOpenMobile(true); + }, [accounts, setOpenMobile]); + + if (isPending) { + return; + } + + return ( + + + + + + + + + + + + + + ); +} diff --git a/web/src/components/app-sidebar/nav-main.tsx b/web/src/components/app-sidebar/nav-main.tsx new file mode 100644 index 00000000..9d496e6f --- /dev/null +++ b/web/src/components/app-sidebar/nav-main.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { ChevronRight, type LucideIcon } from "lucide-react"; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "~/components/ui/collapsible"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "~/components/ui/sidebar"; + +export function NavMain({ + items, +}: { + items: { + title: string; + url: string; + icon: LucideIcon; + isActive?: boolean; + items?: { + title: string; + url: string; + }[]; + }[]; +}) { + return ( + + Platform + + {items.map((item) => ( + + + + + + {item.title} + + + {item.items?.length ? ( + <> + + + + Toggle + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + ) : null} + + + ))} + + + ); +} diff --git a/web/src/components/app-sidebar/nav-user.tsx b/web/src/components/app-sidebar/nav-user.tsx new file mode 100644 index 00000000..c8e15114 --- /dev/null +++ b/web/src/components/app-sidebar/nav-user.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles, +} from "lucide-react"; + +import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "~/components/ui/sidebar"; + +export function NavUser({ + user, +}: { + user: { + name: string; + email: string; + avatar: string; + }; +}) { + const { isMobile } = useSidebar(); + + return ( + + + + + + + + CN + +
+ {user.name} + {user.email} +
+ +
+
+ + +
+ + + CN + +
+ {user.name} + {user.email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
+
+
+
+ ); +} diff --git a/web/src/components/applications/index.tsx b/web/src/components/applications/index.tsx new file mode 100644 index 00000000..d73c792d --- /dev/null +++ b/web/src/components/applications/index.tsx @@ -0,0 +1,4 @@ +// export default function Application() { +// const +// return +// } diff --git a/web/src/components/sign-in/form.tsx b/web/src/components/sign-in/form.tsx index a7c51651..f8c81963 100644 --- a/web/src/components/sign-in/form.tsx +++ b/web/src/components/sign-in/form.tsx @@ -3,7 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { GitHubLogoIcon } from "@radix-ui/react-icons"; import { useMutation } from "@tanstack/react-query"; -import { useRouter } from "@tanstack/react-router"; +import { Link, useRouter } from "@tanstack/react-router"; import { AxiosError } from "axios"; import * as React from "react"; import { useForm } from "react-hook-form"; @@ -11,6 +11,7 @@ import { toast } from "sonner"; import { z } from "zod"; import { signInMutationFn } from "~/actions/auth/sign-in"; +import { setUser } from "~/global-state/persistant-storage/token"; import { cn } from "~/lib/utils"; import { emailPassSignInSchema } from "~/schema/auth"; @@ -18,13 +19,13 @@ import { Button } from "../ui/button"; import { Form, FormField } from "../ui/form"; import { Input } from "../ui/input"; import { PasswordInput } from "../ui/password-input"; +import { Separator } from "../ui/separator"; type UserAuthFormProps = React.HTMLAttributes; type EmailSignInSchema = z.infer; export function SignInForm({ className, ...props }: UserAuthFormProps) { const router = useRouter(); - const form = useForm({ resolver: zodResolver(emailPassSignInSchema), }); @@ -32,7 +33,9 @@ export function SignInForm({ className, ...props }: UserAuthFormProps) { const { mutate, isPending } = useMutation({ mutationFn: signInMutationFn, onSuccess: (res) => { - toast.success(res.message); + console.log(res); + setUser(res); + toast.success("Logged in successfully"); router.navigate({ to: "/" }); }, onError: (err) => { @@ -109,6 +112,17 @@ export function SignInForm({ className, ...props }: UserAuthFormProps) { + + + Don't have an account?{" "} + + Sign up + + + ); } diff --git a/web/src/components/sign-up/form.tsx b/web/src/components/sign-up/form.tsx index f8fd20ce..f9c90cee 100644 --- a/web/src/components/sign-up/form.tsx +++ b/web/src/components/sign-up/form.tsx @@ -1,9 +1,7 @@ -"use client"; - import { zodResolver } from "@hookform/resolvers/zod"; import { GitHubLogoIcon } from "@radix-ui/react-icons"; import { useMutation } from "@tanstack/react-query"; -import { useRouter } from "@tanstack/react-router"; +import { Link, useRouter } from "@tanstack/react-router"; import { AxiosError } from "axios"; import * as React from "react"; import { useForm } from "react-hook-form"; @@ -128,6 +126,16 @@ export function SignUpForm({ className, ...props }: UserAuthFormProps) { + + + Already have an account?{" "} + + Sign in + + ); } diff --git a/web/src/components/sign-up/verify-otp.tsx b/web/src/components/sign-up/verify-otp.tsx index 377f227b..67df83a7 100644 --- a/web/src/components/sign-up/verify-otp.tsx +++ b/web/src/components/sign-up/verify-otp.tsx @@ -11,6 +11,7 @@ import { VerifyOtpInterface, verifyOtpMutationFn, } from "~/actions/auth/verify-otp"; +import { setUser } from "~/global-state/persistant-storage/token"; import { cn } from "~/lib/utils"; import { verifyOtpSchema } from "~/schema/auth"; @@ -32,7 +33,10 @@ export function VerifyOtpForm({ id, className, ...props }: UserAuthFormProps) { const { mutate, isPending } = useMutation({ mutationFn: verifyOtpMutationFn, - onSuccess: (res) => toast.success(res.message), + onSuccess: (res) => { + setUser(res); + toast.success("OTP verified!"); + }, onError: (err) => { let errMessage = "An unknown error occurred."; if (err instanceof AxiosError && err.response?.data?.error) diff --git a/web/src/components/ui/alert-dialog.tsx b/web/src/components/ui/alert-dialog.tsx index 9c983585..bff9cc77 100644 --- a/web/src/components/ui/alert-dialog.tsx +++ b/web/src/components/ui/alert-dialog.tsx @@ -1,10 +1,10 @@ "use client" -import * as React from "react" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" +import * as React from "react" -import { cn } from "~/lib/utils" import { buttonVariants } from "~/components/ui/button" +import { cn } from "~/lib/utils" const AlertDialog = AlertDialogPrimitive.Root @@ -128,14 +128,14 @@ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName export { AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, + AlertDialogAction, + AlertDialogCancel, AlertDialogContent, - AlertDialogHeader, + AlertDialogDescription, AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + AlertDialogPortal, AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, + AlertDialogTrigger, } diff --git a/web/src/components/ui/avatar.tsx b/web/src/components/ui/avatar.tsx index 706f1778..a24bbc5a 100644 --- a/web/src/components/ui/avatar.tsx +++ b/web/src/components/ui/avatar.tsx @@ -1,5 +1,5 @@ -import * as React from "react" import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from "react" import { cn } from "~/lib/utils" @@ -45,4 +45,4 @@ const AvatarFallback = React.forwardRef< )) AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarFallback,AvatarImage } diff --git a/web/src/components/ui/breadcrumb.tsx b/web/src/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..fc7d5307 --- /dev/null +++ b/web/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" +import { Slot } from "@radix-ui/react-slot" +import * as React from "react" + +import { cn } from "~/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>