package handlers import ( "bookhoard/internal/database" "bookhoard/internal/middleware" "fmt" "net/http" "path/filepath" "strings" "time" jwt "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" ) const ( // Session duration constants // Follows same pattern as refresh_token.go SessionDuration = 7 * 24 * time.Hour // 7 days ) // SessionDurationSec is the session duration in seconds for use in cookies and API responses // Note: This is computed from SessionDuration to avoid magic numbers var SessionDurationSec = int(SessionDuration.Seconds()) type AuthHandler struct { db *database.Queries jwtKey []byte loginAttemptTracker *middleware.LoginAttemptTracker } func NewAuthHandler(db *database.Queries, jwtSecret string, loginAttemptTracker *middleware.LoginAttemptTracker) *AuthHandler { return &AuthHandler{ db: db, jwtKey: []byte(jwtSecret), loginAttemptTracker: loginAttemptTracker, } } type RegisterRequest struct { Email string `form:"email" json:"email" validate:"required,email"` Username string `form:"username" json:"username" validate:"required,min=3,max=50"` Password string `form:"password" json:"password" validate:"required,passwordcomplex"` FirstName string `form:"first_name" json:"first_name,omitempty"` LastName string `form:"last_name" json:"last_name,omitempty"` Role string `form:"role" json:"role,omitempty"` } type LoginRequest struct { Login string `form:"login" json:"login" validate:"required"` // email or username Password string `form:"password" json:"password" validate:"required"` } type AuthResponse struct { Token string `json:"access_token"` RefreshToken string `json:"refresh_token,omitempty"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` User UserProfile `json:"user"` } type UserProfile struct { ID string `json:"id"` Email string `json:"email"` Username string `json:"username"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Role string `json:"role"` } type UpdateProfileRequest struct { FirstName string `json:"first_name,omitempty"` LastName string `json:"last_name,omitempty"` } // Register handles POST /api/auth/register func (h *AuthHandler) Register(c echo.Context) error { email := c.FormValue("email") username := c.FormValue("username") password := c.FormValue("password") firstName := c.FormValue("first_name") lastName := c.FormValue("last_name") role := c.FormValue("role") if email == "" || username == "" || password == "" { req := RegisterRequest{} if err := c.Bind(&req); err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusBadRequest, `
Invalid request
`) } return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusBadRequest, `
`+err.Error()+`
`) } return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } email = req.Email username = req.Username password = req.Password firstName = req.FirstName lastName = req.LastName role = req.Role } req := RegisterRequest{Email: email, Username: username, Password: password, FirstName: firstName, LastName: lastName, Role: role} if err := c.Validate(&req); err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusBadRequest, `
`+err.Error()+`
`) } return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } req.Username = strings.TrimSpace(req.Username) if req.Username == "" { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusBadRequest, `
Username cannot be empty or whitespace
`) } return c.JSON(http.StatusBadRequest, map[string]string{"error": "username cannot be empty or whitespace"}) } if req.Role != "" { req.Role = strings.ToLower(req.Role) } if _, err := h.db.GetUserByEmail(c.Request().Context(), req.Email); err == nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusConflict, `
Email already exists
`) } return c.JSON(http.StatusConflict, map[string]string{"error": "email already exists"}) } if _, err := h.db.GetUserByUsername(c.Request().Context(), req.Username); err == nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusConflict, `
Username already exists
`) } return c.JSON(http.StatusConflict, map[string]string{"error": "username already exists"}) } users, err := h.db.ListUsers(c.Request().Context()) if err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusInternalServerError, `
Failed to check existing users: `+err.Error()+`
`) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check existing users: " + err.Error()}) } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusInternalServerError, `
Failed to hash password
`) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to hash password"}) } adminExists := false for _, u := range users { if u.Role == "admin" { adminExists = true break } } var userRole string if len(users) == 0 { userRole = "admin" } else { userRole = req.Role if userRole == "" { userRole = "user" } if userRole != "user" && userRole != "admin" { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusBadRequest, `
Invalid role. Must be 'user' or 'admin'
`) } return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid role. must be 'user' or 'admin'"}) } if userRole == "admin" && adminExists { user, ok := c.Get("user").(database.Users) if !ok || user.Role != "admin" { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusForbidden, `
Only existing administrators can create admin accounts
`) } return c.JSON(http.StatusForbidden, map[string]string{"error": "only administrators can create admin accounts"}) } } } user, err := h.db.CreateUser(c.Request().Context(), database.CreateUserParams{ Email: req.Email, Username: req.Username, PasswordHash: string(hashedPassword), FirstName: pgtype.Text{String: req.FirstName, Valid: req.FirstName != ""}, LastName: pgtype.Text{String: req.LastName, Valid: req.LastName != ""}, Theme: pgtype.Text{String: "tokyo-night", Valid: true}, Role: userRole, }) if err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusInternalServerError, `
`+err.Error()+`
`) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } accessToken, err := h.generateJWTWithAllClaims( uuid.UUID(user.ID.Bytes).String(), user.Role, user.Email, user.Username, ) if err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusInternalServerError, `
Failed to generate token
`) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"}) } // Set HTTP-only cookie for browser-based authentication cookie := &http.Cookie{ Name: "token", Value: accessToken, Path: "/", HttpOnly: true, Secure: false, // TODO: Set to true in production with HTTPS MaxAge: SessionDurationSec, } c.SetCookie(cookie) _, refreshToken, err := h.CreateRefreshToken(uuid.UUID(user.ID.Bytes)) if err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusInternalServerError, `
Failed to generate refresh token
`) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate refresh token"}) } if c.Request().Header.Get("HX-Request") == "true" { html := fmt.Sprintf(`
Registration successful! Redirecting...
`, accessToken, refreshToken, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username)) return c.HTML(http.StatusCreated, html) } if user.FirstName.Valid { firstName = user.FirstName.String } if user.LastName.Valid { lastName = user.LastName.String } return c.JSON(http.StatusCreated, AuthResponse{ Token: accessToken, RefreshToken: refreshToken, TokenType: "Bearer", ExpiresIn: SessionDurationSec, User: UserProfile{ ID: uuid.UUID(user.ID.Bytes).String(), Email: user.Email, Username: user.Username, FirstName: firstName, LastName: lastName, Role: user.Role, }, }) } // Login handles POST /api/auth/login func (h *AuthHandler) Login(c echo.Context) error { login := c.FormValue("login") password := c.FormValue("password") if login == "" || password == "" { req := LoginRequest{} if err := c.Bind(&req); err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusBadRequest, `
Invalid request
`) } return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusBadRequest, `
`+err.Error()+`
`) } return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } login = req.Login password = req.Password } req := LoginRequest{Login: login, Password: password} if err := c.Validate(&req); err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusBadRequest, `
`+err.Error()+`
`) } return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } ip := c.RealIP() if ip == "" { ip = c.Request().RemoteAddr } locked, remainingTime := h.loginAttemptTracker.IsLocked(login) if locked { errMsg := fmt.Sprintf("Account locked. Try again in %d minutes", int(remainingTime.Minutes())+1) if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusTooManyRequests, `
`+errMsg+`
`) } return c.JSON(http.StatusTooManyRequests, map[string]string{"error": errMsg}) } user, err := h.db.GetUserForLogin(c.Request().Context(), req.Login) if err != nil { locked, remainingTime := h.loginAttemptTracker.RecordFailedAttempt(login) if locked { errMsg := fmt.Sprintf("Too many failed attempts. Account locked for %d minutes", int(remainingTime.Minutes())+1) if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusTooManyRequests, `
`+errMsg+`
`) } return c.JSON(http.StatusTooManyRequests, map[string]string{"error": errMsg}) } if c.Request().Header.Get("HX-Request") == "true" { return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) } return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { locked, remainingTime := h.loginAttemptTracker.RecordFailedAttempt(login) if locked { errMsg := fmt.Sprintf("Too many failed attempts. Account locked for %d minutes", int(remainingTime.Minutes())+1) if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusTooManyRequests, `
`+errMsg+`
`) } return c.JSON(http.StatusTooManyRequests, map[string]string{"error": errMsg}) } if c.Request().Header.Get("HX-Request") == "true" { return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) } return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) } h.loginAttemptTracker.ClearAttempts(login) accessToken, err := h.generateJWTWithAllClaims( uuid.UUID(user.ID.Bytes).String(), user.Role, user.Email, user.Username, ) if err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusInternalServerError, `
Failed to generate token
`) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"}) } // Set HTTP-only cookie for browser-based authentication cookie := &http.Cookie{ Name: "token", Value: accessToken, Path: "/", HttpOnly: true, Secure: false, // TODO: Set to true in production with HTTPS MaxAge: SessionDurationSec, } c.SetCookie(cookie) _, refreshToken, err := h.CreateRefreshToken(uuid.UUID(user.ID.Bytes)) if err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusInternalServerError, `
Failed to generate refresh token
`) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate refresh token"}) } if c.Request().Header.Get("HX-Request") == "true" { redirect := c.FormValue("redirect") if redirect == "" { redirect = "/dashboard" } html := fmt.Sprintf(`
Login successful! Redirecting...
`, accessToken, refreshToken, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s","first_name":"%s","last_name":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username, user.FirstName.String, user.LastName.String), redirect) return c.HTML(http.StatusOK, html) } firstName := "" if user.FirstName.Valid { firstName = user.FirstName.String } lastName := "" if user.LastName.Valid { lastName = user.LastName.String } return c.JSON(http.StatusOK, AuthResponse{ Token: accessToken, RefreshToken: refreshToken, TokenType: "Bearer", ExpiresIn: SessionDurationSec, User: UserProfile{ ID: uuid.UUID(user.ID.Bytes).String(), Email: user.Email, Username: user.Username, FirstName: firstName, LastName: lastName, Role: user.Role, }, }) } // GetProfile handles GET /api/auth/profile func (h *AuthHandler) GetProfile(c echo.Context) error { user := MustGetAuthenticatedUser(c) firstName := "" if user.FirstName.Valid { firstName = user.FirstName.String } lastName := "" if user.LastName.Valid { lastName = user.LastName.String } return c.JSON(http.StatusOK, UserProfile{ ID: uuid.UUID(user.ID.Bytes).String(), Email: user.Email, Username: user.Username, FirstName: firstName, LastName: lastName, Role: user.Role, }) } // UpdateProfile handles PUT /api/auth/profile func (h *AuthHandler) UpdateProfile(c echo.Context) error { user := MustGetAuthenticatedUser(c) var req UpdateProfileRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } err := h.db.UpdateUserProfile(c.Request().Context(), database.UpdateUserProfileParams{ ID: user.ID, FirstName: pgtype.Text{String: req.FirstName, Valid: req.FirstName != ""}, LastName: pgtype.Text{String: req.LastName, Valid: req.LastName != ""}, }) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]string{"message": "profile updated"}) } // ListUsers handles GET /api/auth/users func (h *AuthHandler) ListUsers(c echo.Context) error { users, err := h.db.ListUsers(c.Request().Context()) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, users) } // normalizePath cleans and normalizes folder paths for consistent storage and comparison func normalizePath(path string) string { fmt.Printf("normalizePath input: '%s'\n", path) var cleaned string // Handle home directory expansion (~) if strings.HasPrefix(path, "~/") { // Keep the original path for ~ to preserve user's formatting // Just normalize separators and that's it cleaned = strings.ReplaceAll(path, "\\", "/") } else { // Clean the path to remove redundant separators, ., .. etc. cleaned = filepath.Clean(path) // Convert to consistent path separators (use forward slashes for storage) cleaned = strings.ReplaceAll(cleaned, "\\", "/") // Remove trailing slash unless it's root path if len(cleaned) > 1 && strings.HasSuffix(cleaned, "/") { cleaned = strings.TrimSuffix(cleaned, "/") } } fmt.Printf("normalizePath output: '%s'\n", cleaned) return cleaned } type UpdateThemeRequest struct { Theme string `json:"theme" validate:"required"` } // UpdateTheme handles PUT /api/auth/theme func (h *AuthHandler) UpdateTheme(c echo.Context) error { user := MustGetAuthenticatedUser(c) var req UpdateThemeRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } err := h.db.UpdateUserTheme(c.Request().Context(), database.UpdateUserThemeParams{ ID: user.ID, Theme: pgtype.Text{String: req.Theme, Valid: req.Theme != ""}, }) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]string{"message": "theme updated successfully"}) } type UpdateUsernameRequest struct { Username string `json:"username" validate:"required,min=3,max=50"` } // UpdateUsername handles PUT /api/auth/username func (h *AuthHandler) UpdateUsername(c echo.Context) error { user := MustGetAuthenticatedUser(c) var req UpdateUsernameRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } // Check if username is already taken by another user existingUser, err := h.db.GetUserByUsername(c.Request().Context(), req.Username) if err == nil && existingUser.ID.Bytes != user.ID.Bytes { return c.JSON(http.StatusConflict, map[string]string{"error": "username already taken"}) } // Update username err = h.db.UpdateUsername(c.Request().Context(), database.UpdateUsernameParams{ ID: user.ID, Username: req.Username, }) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]string{"message": "username updated successfully"}) } type UpdateEmailRequest struct { Email string `json:"email" validate:"required,email"` } // UpdateEmail handles PUT /api/auth/email func (h *AuthHandler) UpdateEmail(c echo.Context) error { user := MustGetAuthenticatedUser(c) var req UpdateEmailRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } // Check if email is already taken by another user existingUser, err := h.db.GetUserByEmail(c.Request().Context(), req.Email) if err == nil && existingUser.ID.Bytes != user.ID.Bytes { return c.JSON(http.StatusConflict, map[string]string{"error": "email already taken"}) } // Update email err = h.db.UpdateEmail(c.Request().Context(), database.UpdateEmailParams{ ID: user.ID, Email: req.Email, }) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]string{"message": "email updated successfully"}) } type UpdatePasswordRequest struct { CurrentPassword string `json:"current_password" validate:"required"` NewPassword string `json:"new_password" validate:"required,passwordcomplex"` ConfirmPassword string `json:"confirm_password" validate:"required"` } // UpdatePassword handles PUT /api/auth/password func (h *AuthHandler) UpdatePassword(c echo.Context) error { user := MustGetAuthenticatedUser(c) var req UpdatePasswordRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } // Check if new passwords match if req.NewPassword != req.ConfirmPassword { return c.JSON(http.StatusBadRequest, map[string]string{"error": "new passwords do not match"}) } // Get current user's password hash passwordHash, err := h.db.GetUserPasswordHash(c.Request().Context(), user.ID) if err != nil { if err == pgx.ErrNoRows { return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"}) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get user"}) } // Verify current password if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.CurrentPassword)); err != nil { return c.JSON(http.StatusUnauthorized, map[string]string{"error": "current password is incorrect"}) } // Hash new password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to hash password"}) } // Update password err = h.db.UpdatePassword(c.Request().Context(), database.UpdatePasswordParams{ ID: user.ID, PasswordHash: string(hashedPassword), }) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]string{"message": "password updated successfully"}) } // DeleteAccount handles DELETE /api/auth/account // Supports self-deletion or admin deletion of other users func (h *AuthHandler) DeleteAccount(c echo.Context) error { currentUser := MustGetAuthenticatedUser(c) // Get target user ID from query parameter (for admin override) or use current user targetUserID := c.QueryParam("user_id") var targetUserUUID pgtype.UUID // If admin override is used, validate admin and use target if targetUserID != "" { // Admin override mode - check if current user is admin if currentUser.Role != "admin" { return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"}) } parsedUUID, err := uuid.Parse(targetUserID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } targetUserUUID = pgtype.UUID{Bytes: [16]byte(parsedUUID), Valid: true} } else { targetUserUUID = currentUser.ID } // Check if this is the last admin user - prevent deletion users, err := h.db.ListUsers(c.Request().Context()) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check existing users"}) } // Count admin users and identify the user to be deleted adminCount := 0 targetUserRole := "" for _, user := range users { if user.Role == "admin" { adminCount++ } // Find target user details if user.ID.Bytes == targetUserUUID.Bytes { targetUserRole = user.Role } } // Prevent deletion if target user is admin and this is the last admin if targetUserRole == "admin" && adminCount == 1 { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusBadRequest, `
Cannot delete the last admin account
`) } return c.JSON(http.StatusBadRequest, map[string]string{"error": "cannot delete the last admin account"}) } // Delete user (this will cascade to delete all related data) err = h.db.DeleteUser(c.Request().Context(), targetUserUUID) if err != nil { if err == pgx.ErrNoRows { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusNotFound, `
User not found
`) } return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"}) } if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusInternalServerError, `
Failed to delete account
`) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } // Create success message based on context var message string if targetUserID != "" && targetUserUUID.Bytes != currentUser.ID.Bytes { message = "user account deleted successfully" } else { message = "account deleted successfully" } return c.JSON(http.StatusOK, map[string]string{"message": message}) } type UpdateUserMaxDevicesRequest struct { MaxDevices int32 `json:"max_devices" validate:"required,min=1,max=100"` } // UpdateUserMaxDevices handles PUT /api/auth/users/:id/max-devices (admin only) func (h *AuthHandler) UpdateUserMaxDevices(c echo.Context) error { userID := c.Param("id") if userID == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "user id required"}) } var req UpdateUserMaxDevicesRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } user, err := h.db.UpdateUserMaxDevices(c.Request().Context(), database.UpdateUserMaxDevicesParams{ ID: pgtype.UUID{Bytes: userUUID, Valid: true}, MaxDevices: pgtype.Int4{Int32: req.MaxDevices, Valid: true}, }) if err != nil { if err == pgx.ErrNoRows { return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"}) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } _ = user // Suppress unused variable warning when sqlc returns user return c.JSON(http.StatusOK, map[string]string{"message": "max devices updated"}) } // AdminMiddleware checks if the user has admin role func AdminMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { userRole, exists := c.Get("user_role").(string) if !exists || userRole != "admin" { return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"}) } return next(c) } } func (h *AuthHandler) generateJWTWithAllClaims(userID, userRole, userEmail, userUsername string) (string, error) { claims := jwt.MapClaims{ "jti": uuid.New().String(), "user_id": userID, "user_role": userRole, "user_email": userEmail, "user_username": userUsername, "exp": time.Now().Add(SessionDuration).Unix(), "iat": time.Now().Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(h.jwtKey) } func (h *AuthHandler) generateJWTWithRole(userID, userRole string) (string, error) { return h.generateJWTWithAllClaims(userID, userRole, "", "") } func (h *AuthHandler) generateJWT(userID string) (string, error) { return h.generateJWTWithRole(userID, "user") }