From c5ed242f124a58a59932540ffbb5948a345efead Mon Sep 17 00:00:00 2001 From: Sudarsh1010 Date: Fri, 15 Nov 2024 09:02:48 +0530 Subject: [PATCH 1/2] add zustand and integrate with cookies to store user profile data, and authenticated layout --- internal/controllers/auth_controller.go | 10 +- internal/models/main.go | 2 +- internal/services/session_service.go | 13 +- internal/utils/session_helpers.go | 17 +- web/package.json | 2 + web/pnpm-lock.yaml | 364 ++++++++++-------- web/src/actions/auth/sign-in.ts | 9 +- web/src/actions/auth/verify-otp.ts | 7 +- web/src/actions/auth/verify-token.ts | 8 +- web/src/axios.ts | 4 +- web/src/components/sign-in/form.tsx | 20 +- web/src/components/sign-up/form.tsx | 12 +- web/src/components/sign-up/verify-otp.tsx | 6 +- .../global-state/persistant-storage/token.ts | 73 ++++ web/src/global-state/zustand.ts | 18 + web/src/route-tree.gen.ts | 82 ++-- web/src/routes/_authenticated.tsx | 48 +++ web/src/routes/{ => _authenticated}/index.tsx | 5 +- web/src/schema/user.ts | 14 + 19 files changed, 485 insertions(+), 229 deletions(-) create mode 100644 web/src/global-state/persistant-storage/token.ts create mode 100644 web/src/global-state/zustand.ts create mode 100644 web/src/routes/_authenticated.tsx rename web/src/routes/{ => _authenticated}/index.tsx (51%) create mode 100644 web/src/schema/user.ts diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go index 8f5edf2e..cea7852f 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" @@ -65,7 +64,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 +75,9 @@ func (ac *AuthController) SignIn(c *fiber.Ctx) error { JSON(fiber.Map{"error": "Something went wrong, Failed to create session"}) } + 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,11 +149,13 @@ 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) + fmt.Print("\n") + fmt.Print(sessionID) if sessionID == "" { return c. Status(fiber.StatusUnauthorized). diff --git a/internal/models/main.go b/internal/models/main.go index bc297c16..9473356a 100644 --- a/internal/models/main.go +++ b/internal/models/main.go @@ -12,7 +12,7 @@ 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) { diff --git a/internal/services/session_service.go b/internal/services/session_service.go index 56221722..ef25f1d0 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" ) @@ -22,22 +21,18 @@ func NewSessionService(redisRepo *repositories.RedisRepository, userRepo *reposi } 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..a21a2425 100644 --- a/internal/utils/session_helpers.go +++ b/internal/utils/session_helpers.go @@ -1,7 +1,6 @@ package utils import ( - "fmt" "time" "github.com/gofiber/fiber/v2" @@ -10,16 +9,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,11 +19,9 @@ 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: "/", }) } diff --git a/web/package.json b/web/package.json index 022ba662..8a7d066a 100644 --- a/web/package.json +++ b/web/package.json @@ -26,6 +26,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", @@ -41,6 +42,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..64511f24 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -10,40 +10,40 @@ 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-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-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 +55,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,7 +76,7 @@ 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 @@ -85,17 +88,20 @@ importers: 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 +116,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 +152,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 @@ -1183,6 +1189,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 +1710,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==} @@ -2597,9 +2610,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 +2658,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 +2666,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 +2726,110 @@ 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) - - '@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)': - 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) + 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))(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-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-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) + '@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) - '@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-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-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) - '@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-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 +2838,150 @@ 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) - '@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/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) - - '@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)': - 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) + 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 +3091,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 +3108,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 +3162,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 +3196,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 +3211,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 +3247,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 +3271,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 +3526,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 +3557,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 +3594,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 +3727,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 +3764,8 @@ snapshots: jiti@1.21.6: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -3798,7 +3852,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 +3924,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 +3972,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 +4066,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 +4181,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 +4211,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: @@ -4205,7 +4266,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/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 index e93c6aaf..1faf9aed 100644 --- a/web/src/actions/auth/verify-token.ts +++ b/web/src/actions/auth/verify-token.ts @@ -1,7 +1,7 @@ import apiClient from "~/axios"; +import { UserInterface } from "~/schema/user"; -export const verifyTokenQueryFn = async () => +export const verifyToken = async () => await apiClient - .get("auth/verify-token") - .then((res) => console.log(res.data)) - .catch(console.log); + .get("auth/verify-token") + .then((res) => res.data); 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/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..40cba915 100644 --- a/web/src/components/sign-up/form.tsx +++ b/web/src/components/sign-up/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"; @@ -128,6 +128,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/global-state/persistant-storage/token.ts b/web/src/global-state/persistant-storage/token.ts new file mode 100644 index 00000000..a8e7a277 --- /dev/null +++ b/web/src/global-state/persistant-storage/token.ts @@ -0,0 +1,73 @@ +import Cookies from "js-cookie"; +import { create } from "zustand"; +import { persist, StorageValue } from "zustand/middleware"; + +import { verifyToken } from "~/actions/auth/verify-token"; +import { UserInterface } from "~/schema/user"; + +import { createSelectors } from "../zustand"; + +interface UserStoreInterface { + data: UserInterface | null; + logout: () => void; + setData: (data: UserInterface) => void; +} + +const _useUserStore = create()( + persist( + (set) => ({ + data: null, + signIn: (data: UserInterface) => { + set({ data }); + }, + setData: (data: UserInterface) => set({ data }), + setProfileImage: (profileImage: string) => + set((state) => ({ + data: state.data + ? { ...state.data, profile_image: profileImage } + : null, + })), + logout: () => { + Cookies.remove("user-storage"); + set({ data: null }); + window.location.reload(); + }, + }), + { + name: "user-storage", + storage: { + getItem: async (name) => { + try { + const str = Cookies.get(name); + let data: StorageValue; + if (!str) { + data = { + state: await verifyToken(), + version: 0, + }; + setUser(data.state); + } else data = JSON.parse(str); + return data; + } catch { + logout(); + return null; + } + }, + setItem: (name, user: StorageValue) => { + const str = JSON.stringify(user); + Cookies.set(name, str); + }, + removeItem: (name) => Cookies.remove(name), + }, + }, + ), +); + +const useUserStore = createSelectors(_useUserStore); + +export const getUser = () => _useUserStore.getState().data; +export const setUser = (data: UserInterface) => + _useUserStore.getState().setData(data); +export const logout = () => _useUserStore.getState().logout(); + +export default useUserStore; diff --git a/web/src/global-state/zustand.ts b/web/src/global-state/zustand.ts new file mode 100644 index 00000000..9cae955e --- /dev/null +++ b/web/src/global-state/zustand.ts @@ -0,0 +1,18 @@ +import type { StoreApi, UseBoundStore } from "zustand"; + +type WithSelectors = S extends { getState: () => infer T } + ? S & { use: { [K in keyof T]: () => T[K] } } + : never; + +export const createSelectors = >>( + _store: S, +) => { + const store = _store as WithSelectors; + store.use = {}; + for (const k of Object.keys(store.getState())) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]); + } + + return store; +}; diff --git a/web/src/route-tree.gen.ts b/web/src/route-tree.gen.ts index 03f4cade..d3bd4452 100644 --- a/web/src/route-tree.gen.ts +++ b/web/src/route-tree.gen.ts @@ -11,23 +11,29 @@ // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as AuthenticatedImport } from './routes/_authenticated' import { Route as AuthImport } from './routes/_auth' -import { Route as IndexImport } from './routes/index' +import { Route as AuthenticatedIndexImport } from './routes/_authenticated/index' import { Route as AuthSignUpImport } from './routes/_auth/sign-up' import { Route as AuthSignInImport } from './routes/_auth/sign-in' import { Route as AuthVerifyOtpIdImport } from './routes/_auth/verify-otp/$id' // Create/Update Routes +const AuthenticatedRoute = AuthenticatedImport.update({ + id: '/_authenticated', + getParentRoute: () => rootRoute, +} as any) + const AuthRoute = AuthImport.update({ id: '/_auth', getParentRoute: () => rootRoute, } as any) -const IndexRoute = IndexImport.update({ +const AuthenticatedIndexRoute = AuthenticatedIndexImport.update({ id: '/', path: '/', - getParentRoute: () => rootRoute, + getParentRoute: () => AuthenticatedRoute, } as any) const AuthSignUpRoute = AuthSignUpImport.update({ @@ -52,13 +58,6 @@ const AuthVerifyOtpIdRoute = AuthVerifyOtpIdImport.update({ declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexImport - parentRoute: typeof rootRoute - } '/_auth': { id: '/_auth' path: '' @@ -66,6 +65,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthImport parentRoute: typeof rootRoute } + '/_authenticated': { + id: '/_authenticated' + path: '' + fullPath: '' + preLoaderRoute: typeof AuthenticatedImport + parentRoute: typeof rootRoute + } '/_auth/sign-in': { id: '/_auth/sign-in' path: '/sign-in' @@ -80,6 +86,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthSignUpImport parentRoute: typeof AuthImport } + '/_authenticated/': { + id: '/_authenticated/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof AuthenticatedIndexImport + parentRoute: typeof AuthenticatedImport + } '/_auth/verify-otp/$id': { id: '/_auth/verify-otp/$id' path: '/verify-otp/$id' @@ -106,54 +119,68 @@ const AuthRouteChildren: AuthRouteChildren = { const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren) +interface AuthenticatedRouteChildren { + AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute +} + +const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { + AuthenticatedIndexRoute: AuthenticatedIndexRoute, +} + +const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren( + AuthenticatedRouteChildren, +) + export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '': typeof AuthRouteWithChildren + '': typeof AuthenticatedRouteWithChildren '/sign-in': typeof AuthSignInRoute '/sign-up': typeof AuthSignUpRoute + '/': typeof AuthenticatedIndexRoute '/verify-otp/$id': typeof AuthVerifyOtpIdRoute } export interface FileRoutesByTo { - '/': typeof IndexRoute '': typeof AuthRouteWithChildren '/sign-in': typeof AuthSignInRoute '/sign-up': typeof AuthSignUpRoute + '/': typeof AuthenticatedIndexRoute '/verify-otp/$id': typeof AuthVerifyOtpIdRoute } export interface FileRoutesById { __root__: typeof rootRoute - '/': typeof IndexRoute '/_auth': typeof AuthRouteWithChildren + '/_authenticated': typeof AuthenticatedRouteWithChildren '/_auth/sign-in': typeof AuthSignInRoute '/_auth/sign-up': typeof AuthSignUpRoute + '/_authenticated/': typeof AuthenticatedIndexRoute '/_auth/verify-otp/$id': typeof AuthVerifyOtpIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '' | '/sign-in' | '/sign-up' | '/verify-otp/$id' + fullPaths: '' | '/sign-in' | '/sign-up' | '/' | '/verify-otp/$id' fileRoutesByTo: FileRoutesByTo - to: '/' | '' | '/sign-in' | '/sign-up' | '/verify-otp/$id' + to: '' | '/sign-in' | '/sign-up' | '/' | '/verify-otp/$id' id: | '__root__' - | '/' | '/_auth' + | '/_authenticated' | '/_auth/sign-in' | '/_auth/sign-up' + | '/_authenticated/' | '/_auth/verify-otp/$id' fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute AuthRoute: typeof AuthRouteWithChildren + AuthenticatedRoute: typeof AuthenticatedRouteWithChildren } const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, AuthRoute: AuthRouteWithChildren, + AuthenticatedRoute: AuthenticatedRouteWithChildren, } export const routeTree = rootRoute @@ -166,13 +193,10 @@ export const routeTree = rootRoute "__root__": { "filePath": "__root.tsx", "children": [ - "/", - "/_auth" + "/_auth", + "/_authenticated" ] }, - "/": { - "filePath": "index.tsx" - }, "/_auth": { "filePath": "_auth.tsx", "children": [ @@ -181,6 +205,12 @@ export const routeTree = rootRoute "/_auth/verify-otp/$id" ] }, + "/_authenticated": { + "filePath": "_authenticated.tsx", + "children": [ + "/_authenticated/" + ] + }, "/_auth/sign-in": { "filePath": "_auth/sign-in.tsx", "parent": "/_auth" @@ -189,6 +219,10 @@ export const routeTree = rootRoute "filePath": "_auth/sign-up.tsx", "parent": "/_auth" }, + "/_authenticated/": { + "filePath": "_authenticated/index.tsx", + "parent": "/_authenticated" + }, "/_auth/verify-otp/$id": { "filePath": "_auth/verify-otp/$id.tsx", "parent": "/_auth" diff --git a/web/src/routes/_authenticated.tsx b/web/src/routes/_authenticated.tsx new file mode 100644 index 00000000..85d2a5a4 --- /dev/null +++ b/web/src/routes/_authenticated.tsx @@ -0,0 +1,48 @@ +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, Outlet, useRouter } from "@tanstack/react-router"; +import { Loader } from "lucide-react"; +import { useEffect } from "react"; + +import { verifyToken } from "~/actions/auth/verify-token"; +import { setUser } from "~/global-state/persistant-storage/token"; + +export const Route = createFileRoute("/_authenticated")({ + component: RouteComponent, +}); + +function RouteComponent() { + const router = useRouter(); + + const { + data: user, + isPending, + isError, + } = useQuery({ + queryKey: ["verify-token-app-layout"], + queryFn: verifyToken, + }); + + useEffect(() => { + if (user) setUser(user); + }, [user]); + + if (isPending) { + return ( +
+ +
+ ); + } + + console.log(isError); + if (isError) { + router.navigate({ + replace: true, + to: "/sign-in", + search: { redirect: location.href }, + }); + return; + } + + return ; +} diff --git a/web/src/routes/index.tsx b/web/src/routes/_authenticated/index.tsx similarity index 51% rename from web/src/routes/index.tsx rename to web/src/routes/_authenticated/index.tsx index d6d3c332..a221e067 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/_authenticated/index.tsx @@ -1,12 +1,9 @@ import { createFileRoute } from "@tanstack/react-router"; -import { verifyTokenQueryFn } from "~/actions/auth/verify-token"; - -export const Route = createFileRoute("/")({ +export const Route = createFileRoute("/_authenticated/")({ component: RouteComponent, }); function RouteComponent() { - verifyTokenQueryFn(); return "Hello /!"; } diff --git a/web/src/schema/user.ts b/web/src/schema/user.ts new file mode 100644 index 00000000..63cbdd51 --- /dev/null +++ b/web/src/schema/user.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +export const userSchema = z.object({ + last_login: z.string().datetime(), + created_at: z.string().datetime(), + updated_at: z.string().datetime(), + email: z.string().email(), + first_name: z.string(), + last_name: z.string().optional(), + is_verified: z.boolean(), + is_active: z.boolean(), +}); + +export type UserInterface = z.infer; From 799c734c31b75add5c70f3b58896a741a8327028 Mon Sep 17 00:00:00 2001 From: Sudarsh1010 Date: Sun, 24 Nov 2024 16:54:30 +0530 Subject: [PATCH 2/2] temp commit --- docker-compose.yml | 12 + internal/app/container.go | 27 +- internal/app/controllers.go | 8 +- internal/app/middlewares.go | 15 + internal/constants/constants.go | 3 + internal/controllers/account_controller.go | 55 +++ internal/controllers/applicaton_controller.go | 57 +++ internal/controllers/auth_controller.go | 23 +- internal/database/database.go | 39 +- internal/middlewares/auth_middleware.go | 42 +++ internal/models/account.go | 48 +++ internal/models/application.go | 27 ++ internal/models/application_auth_provider.go | 29 ++ internal/models/application_environment.go | 22 ++ internal/models/main.go | 4 +- internal/models/user.go | 55 ++- internal/repositories/account_repository.go | 68 ++++ .../repositories/application_repository.go | 53 +++ .../repositories/user_account_repository.go | 26 ++ internal/repositories/user_repository.go | 13 - internal/server/routes.go | 12 +- internal/server/server.go | 3 + internal/services/account_service.go | 52 +++ internal/services/application_service.go | 65 ++++ internal/services/auth_service.go | 15 +- internal/services/session_service.go | 5 +- internal/utils/session_helpers.go | 11 +- internal/validators/account.go | 31 ++ internal/validators/application.go | 31 ++ internal/validators/{sign_up.go => auth.go} | 15 +- internal/validators/sign_in.go | 6 - internal/validators/verify_otp.go | 6 - web/package.json | 3 + web/pnpm-lock.yaml | 190 ++++++++++ web/src/actions/accounts/index.ts | 10 + web/src/actions/accounts/mutations.ts | 12 + web/src/actions/accounts/query-options.ts | 8 + web/src/actions/auth/profile.ts | 5 + web/src/actions/auth/verify-token.ts | 7 - web/src/components/account/create.tsx | 82 ++++ web/src/components/account/switcher.tsx | 97 +++++ web/src/components/app-sidebar/index.tsx | 180 +++++++++ web/src/components/app-sidebar/nav-main.tsx | 78 ++++ web/src/components/app-sidebar/nav-user.tsx | 110 ++++++ web/src/components/applications/index.tsx | 4 + web/src/components/sign-up/form.tsx | 2 - web/src/components/ui/alert-dialog.tsx | 18 +- web/src/components/ui/avatar.tsx | 4 +- web/src/components/ui/breadcrumb.tsx | 115 ++++++ web/src/components/ui/collapsible.tsx | 9 + web/src/components/ui/credenza.tsx | 153 ++++++++ web/src/components/ui/dialog.tsx | 85 +++-- web/src/components/ui/drawer.tsx | 118 ++++++ web/src/components/ui/dropdown-menu.tsx | 199 ++++++++++ web/src/components/ui/input.tsx | 7 +- web/src/components/ui/label.tsx | 2 +- web/src/components/ui/separator.tsx | 4 +- web/src/components/ui/sheet.tsx | 14 +- web/src/components/ui/sidebar.tsx | 352 +++++++++--------- web/src/components/ui/table.tsx | 6 +- web/src/components/ui/tooltip.tsx | 6 +- web/src/global-state/accounts/index.ts | 23 ++ .../persistant-storage/selected-account.ts | 46 +++ .../global-state/persistant-storage/token.ts | 14 +- web/src/hooks/use-media-query.ts | 19 + web/src/routes/_authenticated.tsx | 58 ++- web/src/routes/_authenticated/index.tsx | 2 +- web/src/schema/account.ts | 22 ++ web/src/styles/index.css | 13 + web/tailwind.config.js | 128 +++---- 70 files changed, 2677 insertions(+), 406 deletions(-) create mode 100644 internal/app/middlewares.go create mode 100644 internal/constants/constants.go create mode 100644 internal/controllers/account_controller.go create mode 100644 internal/controllers/applicaton_controller.go create mode 100644 internal/middlewares/auth_middleware.go create mode 100644 internal/models/account.go create mode 100644 internal/models/application.go create mode 100644 internal/models/application_auth_provider.go create mode 100644 internal/models/application_environment.go create mode 100644 internal/repositories/account_repository.go create mode 100644 internal/repositories/application_repository.go create mode 100644 internal/repositories/user_account_repository.go create mode 100644 internal/services/account_service.go create mode 100644 internal/services/application_service.go create mode 100644 internal/validators/account.go create mode 100644 internal/validators/application.go rename internal/validators/{sign_up.go => auth.go} (84%) delete mode 100644 internal/validators/sign_in.go delete mode 100644 internal/validators/verify_otp.go create mode 100644 web/src/actions/accounts/index.ts create mode 100644 web/src/actions/accounts/mutations.ts create mode 100644 web/src/actions/accounts/query-options.ts create mode 100644 web/src/actions/auth/profile.ts delete mode 100644 web/src/actions/auth/verify-token.ts create mode 100644 web/src/components/account/create.tsx create mode 100644 web/src/components/account/switcher.tsx create mode 100644 web/src/components/app-sidebar/index.tsx create mode 100644 web/src/components/app-sidebar/nav-main.tsx create mode 100644 web/src/components/app-sidebar/nav-user.tsx create mode 100644 web/src/components/applications/index.tsx create mode 100644 web/src/components/ui/breadcrumb.tsx create mode 100644 web/src/components/ui/collapsible.tsx create mode 100644 web/src/components/ui/credenza.tsx create mode 100644 web/src/components/ui/drawer.tsx create mode 100644 web/src/components/ui/dropdown-menu.tsx create mode 100644 web/src/global-state/accounts/index.ts create mode 100644 web/src/global-state/persistant-storage/selected-account.ts create mode 100644 web/src/hooks/use-media-query.ts create mode 100644 web/src/schema/account.ts 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 cea7852f..14b79fea 100644 --- a/internal/controllers/auth_controller.go +++ b/internal/controllers/auth_controller.go @@ -52,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, @@ -75,6 +78,7 @@ 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(user) @@ -152,22 +156,7 @@ func (ac *AuthController) VerifyOTP(c *fiber.Ctx) error { return c.JSON(user) } -func (ac *AuthController) VerifyTokenHandler(c *fiber.Ctx) error { - sessionID := utils.GetSessionCookie(c) - fmt.Print("\n") - fmt.Print(sessionID) - 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 9473356a..4808484e 100644 --- a/internal/models/main.go +++ b/internal/models/main.go @@ -16,6 +16,6 @@ type Base struct { } 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 ef25f1d0..8ebd134a 100644 --- a/internal/services/session_service.go +++ b/internal/services/session_service.go @@ -16,7 +16,10 @@ 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} } diff --git a/internal/utils/session_helpers.go b/internal/utils/session_helpers.go index a21a2425..e95331dc 100644 --- a/internal/utils/session_helpers.go +++ b/internal/utils/session_helpers.go @@ -1,6 +1,8 @@ package utils import ( + "keizer-auth/internal/constants" + "keizer-auth/internal/models" "time" "github.com/gofiber/fiber/v2" @@ -21,10 +23,17 @@ func SetSessionCookie(c *fiber.Ctx, sessionID string) { HTTPOnly: true, Secure: false, SameSite: fiber.CookieSameSiteNoneMode, - // TODO: handle domain }) } 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 8a7d066a..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", @@ -35,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" }, diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 64511f24..733a7542 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -17,9 +17,15 @@ importers: '@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))(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))(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) @@ -83,6 +89,9 @@ importers: 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 @@ -682,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: @@ -722,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: @@ -735,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: @@ -784,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: @@ -836,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: @@ -2266,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} @@ -2761,6 +2850,34 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 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-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: react: 18.3.1 @@ -2801,6 +2918,12 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 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 @@ -2814,6 +2937,21 @@ snapshots: '@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: react: 18.3.1 @@ -2851,6 +2989,32 @@ snapshots: '@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))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2898,6 +3062,23 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 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/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) @@ -4230,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 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/verify-token.ts b/web/src/actions/auth/verify-token.ts deleted file mode 100644 index 1faf9aed..00000000 --- a/web/src/actions/auth/verify-token.ts +++ /dev/null @@ -1,7 +0,0 @@ -import apiClient from "~/axios"; -import { UserInterface } from "~/schema/user"; - -export const verifyToken = async () => - await apiClient - .get("auth/verify-token") - .then((res) => res.data); 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-up/form.tsx b/web/src/components/sign-up/form.tsx index 40cba915..f9c90cee 100644 --- a/web/src/components/sign-up/form.tsx +++ b/web/src/components/sign-up/form.tsx @@ -1,5 +1,3 @@ -"use client"; - import { zodResolver } from "@hookform/resolvers/zod"; import { GitHubLogoIcon } from "@radix-ui/react-icons"; import { useMutation } from "@tanstack/react-query"; 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) =>