From 9346b5acb04e016699b9030fe09f1f055163985c Mon Sep 17 00:00:00 2001 From: Arjuna Date: Thu, 29 Jan 2026 21:21:50 +0700 Subject: [PATCH 1/3] feat: invite link created --- Internal/adapters/repository/authRepo.go | 26 +++++++++ Internal/adapters/repository/postgres.go | 1 + Internal/api/Routes/routev1.go | 4 ++ Internal/api/handlers/authHandler.go | 30 +++++++++++ Internal/core/domain/invitationdb.go | 13 +++++ Internal/core/services/authService.go | 68 ++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 8 files changed, 145 insertions(+) create mode 100644 Internal/core/domain/invitationdb.go diff --git a/Internal/adapters/repository/authRepo.go b/Internal/adapters/repository/authRepo.go index d4c80fb..cbe6488 100644 --- a/Internal/adapters/repository/authRepo.go +++ b/Internal/adapters/repository/authRepo.go @@ -24,3 +24,29 @@ func (ar *AuthRepo) LoginRepo(email string) (*domain.User, error) { } return &user, nil } + +func (ar *AuthRepo) CreateInvite(invite *domain.Invitation) error { + if err := ar.DB.Gorm.Create(invite).Error; err != nil { + return err + } + return nil +} + +func (ar *AuthRepo) FindInviteByToken(token string) (*domain.Invitation, error) { + var invite domain.Invitation + if err := ar.DB.Gorm.Where("token = ?", token).First(&invite).Error; err != nil { + return nil, err + } + return &invite, nil +} + +func (ar *AuthRepo) MarkInviteUsed(token string) error { + var invite domain.Invitation + if err := ar.DB.Gorm.Where("token = ?", token).First(&invite).Error; err != nil { + return err + } + if err := ar.DB.Gorm.Model(&invite).Update("used", true).Error; err != nil { + return err + } + return nil +} diff --git a/Internal/adapters/repository/postgres.go b/Internal/adapters/repository/postgres.go index bc02520..e1974f5 100644 --- a/Internal/adapters/repository/postgres.go +++ b/Internal/adapters/repository/postgres.go @@ -40,6 +40,7 @@ func ConnectDB() (*DBContainer, error) { func (db *DBContainer) Migrate() error { err := db.Gorm.AutoMigrate( &domain.User{}, + &domain.Invitation{}, ) if err != nil { log.Fatalf("Failed to migrate users: %v", err) diff --git a/Internal/api/Routes/routev1.go b/Internal/api/Routes/routev1.go index db257b4..15e0161 100644 --- a/Internal/api/Routes/routev1.go +++ b/Internal/api/Routes/routev1.go @@ -40,6 +40,10 @@ func SetupRouterV1(r *gin.Engine, deps Deps) { { user.GET("/me", deps.User.GetUserHandler) } + admin := protected.Group("/admin") + { + admin.POST("/invite", deps.Auth.CreateInviteHandler) + } } } } diff --git a/Internal/api/handlers/authHandler.go b/Internal/api/handlers/authHandler.go index c6a3932..96d1bcf 100644 --- a/Internal/api/handlers/authHandler.go +++ b/Internal/api/handlers/authHandler.go @@ -11,6 +11,11 @@ type AuthHandler struct { Service *services.AuthService } +type InviteInput struct { + Email string `json:"email"` + Role string `json:"role"` +} + func NewAuthHandler(service *services.AuthService) *AuthHandler { return &AuthHandler{Service: service} } @@ -69,3 +74,28 @@ func (ah *AuthHandler) LogoutHandler(c *gin.Context) { "data": nil, }) } + +func (ah *AuthHandler) CreateInviteHandler(c *gin.Context) { + var req InviteInput + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{ + "message": "Invalid Input", + "data": err.Error(), + }) + return + } + if req.Role == "" { + req.Role = "user" + } + link, err := ah.Service.GenerateInviteService(req.Email, req.Role) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": "Invite service failed", + "data": err.Error(), + }) + } + c.JSON(200, gin.H{ + "message": "Invite service success", + "data": link, + }) +} diff --git a/Internal/core/domain/invitationdb.go b/Internal/core/domain/invitationdb.go new file mode 100644 index 0000000..de3dcec --- /dev/null +++ b/Internal/core/domain/invitationdb.go @@ -0,0 +1,13 @@ +package domain + +import "time" + +type Invitation struct { + ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` + Email string `gorm:"size:255; not null" json:"email"` + Token string `gorm:"size:255; not null; unique; index" json:"token"` + Role string `gorm:"default:'user'; size:255; not null" json:"role"` + Used bool `gorm:"default:false" json:"used"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` + ExpiresAt time.Time `json:"expires_at"` +} diff --git a/Internal/core/services/authService.go b/Internal/core/services/authService.go index 848445e..7fee232 100644 --- a/Internal/core/services/authService.go +++ b/Internal/core/services/authService.go @@ -1,12 +1,14 @@ package services import ( + "errors" "time" "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" "github.com/Arjuna-Ragil/Localbase/Internal/config" "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) @@ -21,6 +23,14 @@ type RegisterInput struct { Role string `json:"role" binding:"required"` } +type InviteInput struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + Role string `json:"role" binding:"required"` + Token string `json:"token"` +} + type LoginInput struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` @@ -44,6 +54,33 @@ func (as *AuthService) RegisterService(input *RegisterInput) error { return nil } +func (as *AuthService) InviteRegisterService(input *InviteInput) error { + invite, err := as.VerifyInviteService(input.Token) + if err != nil { + return err + } + + if invite.Email == "" && invite.Email != input.Email { + return errors.New("email does not match") + } + + user := domain.User{ + Username: input.Username, + Email: invite.Email, + Password: input.Password, + Role: invite.Role, + } + + if err := as.Repo.RegisterRepo(&user); err != nil { + return err + } + + if err := as.Repo.MarkInviteUsed(input.Token); err != nil { + return err + } + return nil +} + func (as *AuthService) LoginService(input *LoginInput) (string, error) { cfg := config.LoadConfig() @@ -64,3 +101,34 @@ func (as *AuthService) LoginService(input *LoginInput) (string, error) { } return tokenString, nil } + +func (as *AuthService) GenerateInviteService(email string, role string) (string, error) { + token := uuid.NewString() + expiresAt := time.Now().Add(time.Hour * 24) + invite := domain.Invitation{ + Email: email, + Token: token, + Role: role, + Used: false, + ExpiresAt: expiresAt, + } + if err := as.Repo.CreateInvite(&invite); err != nil { + return "", err + } + linkInvite := "http://localhost:5173/registerinvite?token=" + token //Change link if different + return linkInvite, nil +} + +func (as *AuthService) VerifyInviteService(token string) (*domain.Invitation, error) { + invite, err := as.Repo.FindInviteByToken(token) + if err != nil { + return nil, err + } + if invite.Used == true { + return nil, errors.New("invite already used") + } + if time.Now().After(invite.ExpiresAt) { + return nil, errors.New("invite expired") + } + return invite, nil +} diff --git a/go.mod b/go.mod index 23d07ae..a52399d 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.8.0 // indirect diff --git a/go.sum b/go.sum index 32c3108..10ec40e 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= From e9e883d7e91dbdd017363f0c2aa2ddd3ade74dbc Mon Sep 17 00:00:00 2001 From: Arjuna Date: Fri, 30 Jan 2026 21:11:57 +0700 Subject: [PATCH 2/3] feat: FE integration for adding invitation --- Internal/api/handlers/userHandler.go | 2 +- Internal/core/services/authService.go | 93 +++++------ Lb-web/src/features/Login/hooks/useLogin.ts | 5 +- .../features/Login/services/authService.ts | 13 ++ .../admin-setting/pages/UserManagement.tsx | 145 +++++++++++++++--- .../services/userManagementService.ts | 18 +++ cmd/server/main.go | 8 +- 7 files changed, 208 insertions(+), 76 deletions(-) create mode 100644 Lb-web/src/features/Login/services/authService.ts create mode 100644 Lb-web/src/features/admin-setting/services/userManagementService.ts diff --git a/Internal/api/handlers/userHandler.go b/Internal/api/handlers/userHandler.go index 897e371..ac35ff8 100644 --- a/Internal/api/handlers/userHandler.go +++ b/Internal/api/handlers/userHandler.go @@ -22,7 +22,7 @@ func (uh *UserHandler) GetUserHandler(c *gin.Context) { }) return } - user, err := uh.Service.GetUser(userID.(uint)) + user, err := uh.Service.GetUser(uint(userID.(int))) if err != nil { c.JSON(400, gin.H{ "message": "Unable to get user data", diff --git a/Internal/core/services/authService.go b/Internal/core/services/authService.go index 7fee232..8c7145f 100644 --- a/Internal/core/services/authService.go +++ b/Internal/core/services/authService.go @@ -13,7 +13,8 @@ import ( ) type AuthService struct { - Repo *repository.AuthRepo + AuthRepo *repository.AuthRepo + SysRepo *repository.SystemRepo } type RegisterInput struct { @@ -21,13 +22,6 @@ type RegisterInput struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` Role string `json:"role" binding:"required"` -} - -type InviteInput struct { - Username string `json:"username" binding:"required"` - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` - Role string `json:"role" binding:"required"` Token string `json:"token"` } @@ -36,47 +30,56 @@ type LoginInput struct { Password string `json:"password" binding:"required,min=6"` } -func NewAuthService(repo *repository.AuthRepo) *AuthService { - return &AuthService{Repo: repo} +func NewAuthService(auth *repository.AuthRepo, sys *repository.SystemRepo) *AuthService { + return &AuthService{AuthRepo: auth, SysRepo: sys} } func (as *AuthService) RegisterService(input *RegisterInput) error { - user := domain.User{ - Username: input.Username, - Email: input.Email, - Password: input.Password, - Role: input.Role, - } - err := as.Repo.RegisterRepo(&user) + isInitialized, err := as.SysRepo.IsAppInitialized() if err != nil { return err } - return nil -} -func (as *AuthService) InviteRegisterService(input *InviteInput) error { - invite, err := as.VerifyInviteService(input.Token) - if err != nil { - return err - } - - if invite.Email == "" && invite.Email != input.Email { - return errors.New("email does not match") - } - - user := domain.User{ - Username: input.Username, - Email: invite.Email, - Password: input.Password, - Role: invite.Role, - } - - if err := as.Repo.RegisterRepo(&user); err != nil { - return err - } - - if err := as.Repo.MarkInviteUsed(input.Token); err != nil { - return err + if !isInitialized { + user := domain.User{ + Username: input.Username, + Email: input.Email, + Password: input.Password, + Role: "admin", + } + + if err := as.AuthRepo.RegisterRepo(&user); err != nil { + return err + } + + } else { + if input.Token == "" { + return errors.New("token is required") + } + + invite, err := as.VerifyInviteService(input.Token) + if err != nil { + return err + } + + if invite.Email == "" && invite.Email != input.Email { + return errors.New("email does not match") + } + + user := domain.User{ + Username: input.Username, + Email: invite.Email, + Password: input.Password, + Role: invite.Role, + } + + if err := as.AuthRepo.MarkInviteUsed(input.Token); err != nil { + return err + } + + if err := as.AuthRepo.RegisterRepo(&user); err != nil { + return err + } } return nil } @@ -84,7 +87,7 @@ func (as *AuthService) InviteRegisterService(input *InviteInput) error { func (as *AuthService) LoginService(input *LoginInput) (string, error) { cfg := config.LoadConfig() - user, err := as.Repo.LoginRepo(input.Email) + user, err := as.AuthRepo.LoginRepo(input.Email) if err != nil { return "", err } @@ -112,7 +115,7 @@ func (as *AuthService) GenerateInviteService(email string, role string) (string, Used: false, ExpiresAt: expiresAt, } - if err := as.Repo.CreateInvite(&invite); err != nil { + if err := as.AuthRepo.CreateInvite(&invite); err != nil { return "", err } linkInvite := "http://localhost:5173/registerinvite?token=" + token //Change link if different @@ -120,7 +123,7 @@ func (as *AuthService) GenerateInviteService(email string, role string) (string, } func (as *AuthService) VerifyInviteService(token string) (*domain.Invitation, error) { - invite, err := as.Repo.FindInviteByToken(token) + invite, err := as.AuthRepo.FindInviteByToken(token) if err != nil { return nil, err } diff --git a/Lb-web/src/features/Login/hooks/useLogin.ts b/Lb-web/src/features/Login/hooks/useLogin.ts index 70192eb..95bb01e 100644 --- a/Lb-web/src/features/Login/hooks/useLogin.ts +++ b/Lb-web/src/features/Login/hooks/useLogin.ts @@ -1,6 +1,7 @@ import { useState } from 'react'; -import api from '@/services/api'; + import { useAuth } from '../context/AuthContext'; +import { authService } from '../services/authService'; export const useLogin = () => { const [email, setEmail] = useState(''); @@ -15,7 +16,7 @@ export const useLogin = () => { setError(null); try { - await api.post('/auth/Login', { email, password }); + await authService.login({ email, password }); // Redirect or update global auth state here // For now, assuming successful cookie set by backend login(); // Update auth state, wrapper will handle redirect diff --git a/Lb-web/src/features/Login/services/authService.ts b/Lb-web/src/features/Login/services/authService.ts new file mode 100644 index 0000000..75ba87e --- /dev/null +++ b/Lb-web/src/features/Login/services/authService.ts @@ -0,0 +1,13 @@ +import api from '@/services/api'; + +export interface LoginRequest { + email: string; + password?: string; + // Add other fields if necessary +} + +export const authService = { + login: async (data: LoginRequest) => { + return api.post('/auth/Login', data); + }, +}; diff --git a/Lb-web/src/features/admin-setting/pages/UserManagement.tsx b/Lb-web/src/features/admin-setting/pages/UserManagement.tsx index 7ce821f..f86aed5 100644 --- a/Lb-web/src/features/admin-setting/pages/UserManagement.tsx +++ b/Lb-web/src/features/admin-setting/pages/UserManagement.tsx @@ -1,5 +1,5 @@ import { Button } from "@/components/ui/button"; -import { Plus } from "lucide-react"; +import { Plus, Loader2, Copy, Check } from "lucide-react"; import { Dialog, DialogContent, @@ -11,16 +11,48 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { useState } from "react"; +import { userManagementService } from "../services/userManagementService"; +import { Label } from "@/components/ui/label"; export default function UserManagement() { const [email, setEmail] = useState(""); + const [role, setRole] = useState("user"); const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [inviteLink, setInviteLink] = useState(null); + const [copied, setCopied] = useState(false); + const [error, setError] = useState(null); - const handleInvite = () => { - // Logic to invite user will go here - console.log("Inviting user:", email); - setOpen(false); + const handleInvite = async () => { + if (!email) return; + + setLoading(true); + setError(null); + try { + const response = await userManagementService.inviteUser({ email, role }); + setInviteLink(response.data); + } catch (err) { + console.error("Failed to invite user:", err); + setError("Failed to generate invite link. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleCopyLink = () => { + if (inviteLink) { + navigator.clipboard.writeText(inviteLink); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + const resetForm = () => { setEmail(""); + setRole("user"); + setInviteLink(null); + setError(null); + setLoading(false); }; return ( @@ -30,7 +62,10 @@ export default function UserManagement() {

User Management

Manage system users and their roles.

- + { + setOpen(val); + if (!val) resetForm(); + }}> + + + )} + - + {!inviteLink ? ( + + ) : ( + + )} diff --git a/Lb-web/src/features/admin-setting/services/userManagementService.ts b/Lb-web/src/features/admin-setting/services/userManagementService.ts new file mode 100644 index 0000000..e172518 --- /dev/null +++ b/Lb-web/src/features/admin-setting/services/userManagementService.ts @@ -0,0 +1,18 @@ +import api from '@/services/api'; + +export interface InviteUserResponse { + data: string; + message: string; +} + +export interface InviteUserRequest { + email: string; + role: string; +} + +export const userManagementService = { + inviteUser: async (data: InviteUserRequest): Promise => { + const response = await api.post('/protected/admin/invite', data); + return response.data; + }, +}; diff --git a/cmd/server/main.go b/cmd/server/main.go index a5dc1df..e5025f8 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -52,14 +52,14 @@ func SetupApp(db *repository.DBContainer, cfg *config.Config) Routes.Deps { userService := services.NewUserService(userRepo) userHandler := handlers.NewUserHandler(userService) - authRepo := repository.NewAuthRepo(db) - authService := services.NewAuthService(authRepo) - authHandler := handlers.NewAuthHandler(authService) - systemRepo := repository.NewSystemRepo(db) systemService := services.NewSystemService(systemRepo) systemHandler := handlers.NewSystemHandler(systemService) + authRepo := repository.NewAuthRepo(db) + authService := services.NewAuthService(authRepo, systemRepo) + authHandler := handlers.NewAuthHandler(authService) + return Routes.Deps{ User: userHandler, Auth: authHandler, From a433b1489321a458df775d114ae538c4ba96f616 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Mon, 2 Feb 2026 17:05:09 +0700 Subject: [PATCH 3/3] feat: invited users register & login added --- Internal/api/handlers/authHandler.go | 1 + Internal/api/handlers/userHandler.go | 2 +- Internal/api/middleware/auth.go | 4 - .../Login/components/AuthWrappers.tsx | 18 ++++- .../features/Login/context/AuthContext.tsx | 10 ++- .../src/features/Login/hooks/useRegister.ts | 7 +- Lb-web/src/features/Login/register.tsx | 44 +++++++++- .../features/Login/services/systemService.ts | 8 ++ .../components/SettingsSidebar.tsx | 80 +++++++++++++------ .../admin-setting/pages/Appearance.tsx | 16 ++++ Lb-web/src/main.tsx | 22 ++++- 11 files changed, 171 insertions(+), 41 deletions(-) create mode 100644 Lb-web/src/features/Login/services/systemService.ts create mode 100644 Lb-web/src/features/admin-setting/pages/Appearance.tsx diff --git a/Internal/api/handlers/authHandler.go b/Internal/api/handlers/authHandler.go index 96d1bcf..75506bc 100644 --- a/Internal/api/handlers/authHandler.go +++ b/Internal/api/handlers/authHandler.go @@ -93,6 +93,7 @@ func (ah *AuthHandler) CreateInviteHandler(c *gin.Context) { "message": "Invite service failed", "data": err.Error(), }) + return } c.JSON(200, gin.H{ "message": "Invite service success", diff --git a/Internal/api/handlers/userHandler.go b/Internal/api/handlers/userHandler.go index ac35ff8..897e371 100644 --- a/Internal/api/handlers/userHandler.go +++ b/Internal/api/handlers/userHandler.go @@ -22,7 +22,7 @@ func (uh *UserHandler) GetUserHandler(c *gin.Context) { }) return } - user, err := uh.Service.GetUser(uint(userID.(int))) + user, err := uh.Service.GetUser(userID.(uint)) if err != nil { c.JSON(400, gin.H{ "message": "Unable to get user data", diff --git a/Internal/api/middleware/auth.go b/Internal/api/middleware/auth.go index d274e69..d9a6038 100644 --- a/Internal/api/middleware/auth.go +++ b/Internal/api/middleware/auth.go @@ -73,10 +73,6 @@ func AuthMiddleware(userRepo *repository.UserRepository, cfg *config.Config) gin c.Set("userRole", user.Role) c.Next() - c.JSON(200, gin.H{ - "message": "Success", - "data": " ", - }) } else { c.JSON(401, gin.H{ "message": "Invalid token", diff --git a/Lb-web/src/features/Login/components/AuthWrappers.tsx b/Lb-web/src/features/Login/components/AuthWrappers.tsx index cca6571..a2a2504 100644 --- a/Lb-web/src/features/Login/components/AuthWrappers.tsx +++ b/Lb-web/src/features/Login/components/AuthWrappers.tsx @@ -5,7 +5,7 @@ export const ProtectedRoute = () => { const { isAuthenticated, loading } = useAuth(); if (loading) { - return
Loading...
; + return
Loading...
; } if (!isAuthenticated) { @@ -19,7 +19,7 @@ export const PublicRoute = () => { const { isAuthenticated, loading } = useAuth(); if (loading) { - return
Loading...
; + return
Loading...
; } if (isAuthenticated) { @@ -28,3 +28,17 @@ export const PublicRoute = () => { return ; }; + +export const AdminRoute = () => { + const { role, loading } = useAuth(); + + if (loading) { + return
Loading...
; + } + + if (role !== 'admin') { + return ; + } + + return ; +}; diff --git a/Lb-web/src/features/Login/context/AuthContext.tsx b/Lb-web/src/features/Login/context/AuthContext.tsx index 7c2e2dc..f083f77 100644 --- a/Lb-web/src/features/Login/context/AuthContext.tsx +++ b/Lb-web/src/features/Login/context/AuthContext.tsx @@ -3,6 +3,7 @@ import api from "@/services/api"; interface AuthContextType { isAuthenticated: boolean | null; + role: string | null; loading: boolean; login: () => void; // call this after successful login to update state logout: () => Promise; @@ -12,14 +13,17 @@ const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: ReactNode }) { const [isAuthenticated, setIsAuthenticated] = useState(null); + const [role, setRole] = useState(null); const [loading, setLoading] = useState(true); const checkAuth = async () => { try { - await api.get('/protected/user/me'); + const response = await api.get('/protected/user/me'); setIsAuthenticated(true); + setRole(response.data.data.role); } catch { setIsAuthenticated(false); + setRole(null); } finally { setLoading(false); } @@ -31,6 +35,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const login = () => { setIsAuthenticated(true); + checkAuth(); // Re-fetch user data to get role }; const logout = async () => { @@ -40,11 +45,12 @@ export function AuthProvider({ children }: { children: ReactNode }) { console.error("Logout failed", error); } finally { setIsAuthenticated(false); + setRole(null); } }; return ( - + {children} ); diff --git a/Lb-web/src/features/Login/hooks/useRegister.ts b/Lb-web/src/features/Login/hooks/useRegister.ts index cf3d13e..8ebceec 100644 --- a/Lb-web/src/features/Login/hooks/useRegister.ts +++ b/Lb-web/src/features/Login/hooks/useRegister.ts @@ -10,6 +10,8 @@ export const useRegister = () => { // Assuming for first run, the user being created is likely an Admin/Owner. // The backend might enforce this or we can just send it. + const [token, setToken] = useState(null); + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const navigate = useNavigate(); @@ -24,7 +26,8 @@ export const useRegister = () => { username, email, password, - role + role, + token }); console.log("Registration successful"); // After successful registration, usually redirect to login or auto-login. @@ -51,6 +54,8 @@ export const useRegister = () => { setPassword, role, setRole, + token, + setToken, loading, error, handleRegister, diff --git a/Lb-web/src/features/Login/register.tsx b/Lb-web/src/features/Login/register.tsx index b692fce..eb0b6e4 100644 --- a/Lb-web/src/features/Login/register.tsx +++ b/Lb-web/src/features/Login/register.tsx @@ -9,6 +9,9 @@ import { } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { useNavigate, useSearchParams } from "react-router"; +import { useEffect, useState } from "react"; +import { systemService } from "./services/systemService"; import { useRegister } from "./hooks/useRegister"; export default function Register() { @@ -17,9 +20,48 @@ export default function Register() { email, setEmail, password, setPassword, role, setRole, - loading, error, handleRegister + loading, error, handleRegister, setToken } = useRegister(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const [checking, setChecking] = useState(true); + + useEffect(() => { + const checkAccess = async () => { + const token = searchParams.get("token"); + if (token) { + // If token exists, we assume it's a valid invite (or backend will reject on submit) + // Just allow access to the page + setToken(token); + setChecking(false); + return; + } + + try { + // No token, check if system is already initialized + const isInit = await systemService.checkInitStatus(); + if (isInit) { + // System initialized and no token -> unauthorized + navigate("/login"); + } else { + // System not initialized -> first user setup -> allow + setChecking(false); + } + } catch (err) { + console.error("Failed to check system status", err); + // Fail safe? Maybe redirect to login + navigate("/login"); + } + }; + + checkAccess(); + }, [navigate, searchParams]); + + if (checking) { + return
Checking access...
; + } + return (
{/* Dynamic Background Elements */} diff --git a/Lb-web/src/features/Login/services/systemService.ts b/Lb-web/src/features/Login/services/systemService.ts new file mode 100644 index 0000000..8a247ec --- /dev/null +++ b/Lb-web/src/features/Login/services/systemService.ts @@ -0,0 +1,8 @@ +import api from '@/services/api'; + +export const systemService = { + checkInitStatus: async (): Promise => { + const response = await api.get<{ data: boolean }>('/system/status'); + return response.data.data; + } +}; diff --git a/Lb-web/src/features/admin-setting/components/SettingsSidebar.tsx b/Lb-web/src/features/admin-setting/components/SettingsSidebar.tsx index da4e101..83c7d8a 100644 --- a/Lb-web/src/features/admin-setting/components/SettingsSidebar.tsx +++ b/Lb-web/src/features/admin-setting/components/SettingsSidebar.tsx @@ -1,47 +1,75 @@ -import { Users } from "lucide-react"; +import { Users, Palette } from "lucide-react"; import { Link, useLocation } from "react-router"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; +import { useAuth } from "@/features/Login/context/AuthContext"; -const sidebarItems = [ +const generalItems = [ + { + title: "Appearance", + icon: Palette, + href: "/settings/appearance", + variant: "ghost" + } +]; + +const adminItems = [ { title: "User Management", icon: Users, href: "/settings/users", variant: "ghost" - }, - // Add more items here in the future + } ]; export function SettingsSidebar() { const location = useLocation(); + const { role } = useAuth(); + + const NavItem = ({ item }: { item: any }) => ( + + ); return ( ); diff --git a/Lb-web/src/features/admin-setting/pages/Appearance.tsx b/Lb-web/src/features/admin-setting/pages/Appearance.tsx new file mode 100644 index 0000000..8811b57 --- /dev/null +++ b/Lb-web/src/features/admin-setting/pages/Appearance.tsx @@ -0,0 +1,16 @@ +export default function Appearance() { + return ( +
+
+

Appearance

+

Customize the look and feel of your application.

+
+ +
+
+

Theme settings coming soon...

+
+
+
+ ); +} diff --git a/Lb-web/src/main.tsx b/Lb-web/src/main.tsx index c924d36..e120bda 100644 --- a/Lb-web/src/main.tsx +++ b/Lb-web/src/main.tsx @@ -6,10 +6,11 @@ import Register from './features/Login/register.tsx' import Home from './pages/Home.tsx' import { createBrowserRouter, RouterProvider, Navigate } from 'react-router' import { AuthProvider } from './features/Login/context/AuthContext.tsx' -import { ProtectedRoute, PublicRoute } from './features/Login/components/AuthWrappers.tsx' +import { ProtectedRoute, PublicRoute, AdminRoute } from './features/Login/components/AuthWrappers.tsx' import SettingsLayout from './features/admin-setting/layout/SettingsLayout.tsx' import UserManagement from './features/admin-setting/pages/UserManagement.tsx' +import Appearance from './features/admin-setting/pages/Appearance.tsx' const router = createBrowserRouter([ { @@ -22,6 +23,10 @@ const router = createBrowserRouter([ { path: 'register', element: + }, + { + path: 'registerinvite', + element: } ] }, @@ -38,12 +43,21 @@ const router = createBrowserRouter([ element: , children: [ { - path: 'users', - element: + element: , + children: [ + { + path: 'users', + element: + } + ] + }, + { + path: 'appearance', + element: }, { index: true, - element: + element: } ] }