package handlers import ( "bookmann/internal/database" "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" ) type AuthHandler struct { db *database.Queries jwtKey []byte } func NewAuthHandler(db *database.Queries, jwtSecret string) *AuthHandler { return &AuthHandler{ db: db, jwtKey: []byte(jwtSecret), } } 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,min=6"` 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:"token"` 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 { // Try form data first (HTMX), then JSON (Bruno) 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 == "" { // Fallback to JSON binding 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()}) } // Check if user already exists 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"}) } // Check if this is the first user - if so, make them admin regardless of request 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()}) } // Hash password 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"}) } // Check if any admin users already exist adminExists := false for _, u := range users { if u.Role == "admin" { adminExists = true break } } // Set role - first user is always admin, otherwise validate requested role based on existing admins var userRole string if len(users) == 0 { userRole = "admin" // First user is always admin } else { userRole = req.Role if userRole == "" { userRole = "user" // Default to regular user if not specified } // Validate role for subsequent users 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'"}) } // Role-based restrictions: only admins can create admin users if any admin already exists if userRole == "admin" && adminExists { // Check if current user is admin (requires authentication) userID := c.Get("user_id") if userID == nil { // Not authenticated - cannot create admin user if admins exist 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 existing administrators can create admin accounts"}) } // User is authenticated - check their role userRoleAuth := c.Get("user_role").(string) if userRoleAuth != "admin" { // Authenticated but not admin - cannot create admin accounts if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusForbidden, `
Only administrators can create admin accounts
`) } return c.JSON(http.StatusForbidden, map[string]string{"error": "only administrators can create admin accounts"}) } } } // Create user 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}, // default theme 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()}) } // Generate JWT with user details token, 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"}) } // Check if request is from HTMX if c.Request().Header.Get("HX-Request") == "true" { // Return HTML with script to set token and redirect html := fmt.Sprintf(`
Registration successful! Redirecting...
`, token, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username), token) 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: token, 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 { // Debug logging fmt.Printf("Login request - Content-Type: %s\n", c.Request().Header.Get("Content-Type")) fmt.Printf("Form values - login: %s, password: %s\n", c.FormValue("login"), c.FormValue("password")) // Try form data first (HTMX), then JSON (Bruno) login := c.FormValue("login") password := c.FormValue("password") if login == "" || password == "" { fmt.Printf("Form values empty, trying JSON bind\n") // Fallback to JSON binding req := LoginRequest{} if err := c.Bind(&req); err != nil { fmt.Printf("JSON bind error: %v\n", err) 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 { fmt.Printf("Validation error: %v\n", err) 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 fmt.Printf("JSON bind success - login: %s\n", login) } req := LoginRequest{Login: login, Password: password} if err := c.Validate(&req); err != nil { fmt.Printf("Final validation error: %v\n", err) 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()}) } // Get user by email or username (includes password hash for verification) user, err := h.db.GetUserForLogin(c.Request().Context(), req.Login) if err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusUnauthorized, `
Invalid credentials
`) } return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) } // Check password if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusUnauthorized, `
Invalid credentials
`) } return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) } // Generate JWT with user details token, 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"}) } // Check if request is from HTMX if c.Request().Header.Get("HX-Request") == "true" { // Return HTML with script to set token and redirect html := fmt.Sprintf(`
Login successful! Redirecting...
`, token, 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), token) 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: token, 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 { userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } user, err := h.db.GetUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, 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()}) } 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 { userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } 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: pgtype.UUID{Bytes: userUUID, Valid: true}, 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()}) } type UserList struct { ID string `json:"id"` Email string `json:"email"` Username string `json:"username"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Theme string `json:"theme"` Role string `json:"role"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } var userList []UserList for _, u := range users { theme := "" if u.Theme.Valid { theme = u.Theme.String } firstName := "" if u.FirstName.Valid { firstName = u.FirstName.String } lastName := "" if u.LastName.Valid { lastName = u.LastName.String } createdAt := "" if u.CreatedAt.Valid { createdAt = u.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00") } updatedAt := "" if u.UpdatedAt.Valid { updatedAt = u.UpdatedAt.Time.Format("2006-01-02T15:04:05Z07:00") } userList = append(userList, UserList{ ID: uuid.UUID(u.ID.Bytes).String(), Email: u.Email, Username: u.Username, FirstName: firstName, LastName: lastName, Theme: theme, Role: u.Role, CreatedAt: createdAt, UpdatedAt: updatedAt, }) } return c.JSON(http.StatusOK, userList) } type AddEbookFolderRequest struct { FolderPath string `json:"folder_path" validate:"required"` } // 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 EbookFolderResponse struct { ID string `json:"id"` UserID string `json:"user_id"` FolderPath string `json:"folder_path"` CreatedAt string `json:"created_at"` } type DeleteEbookFolderRequest struct { FolderPath string `json:"folder_path" validate:"required"` } // AddEbookFolder handles POST /api/auth/ebook-folders (DEPRECATED - use libraries instead) func (h *AuthHandler) AddEbookFolder(c echo.Context) error { return c.JSON(http.StatusGone, map[string]string{"error": "Ebook folders are deprecated. Please use the library system instead."}) } // GetEbookFolders handles GET /api/auth/ebook-folders (DEPRECATED - use libraries instead) func (h *AuthHandler) GetEbookFolders(c echo.Context) error { return c.JSON(http.StatusGone, map[string]string{"error": "Ebook folders are deprecated. Please use library system instead."}) } // DeleteEbookFolder handles DELETE /api/auth/ebook-folders (DEPRECATED - use libraries instead) func (h *AuthHandler) DeleteEbookFolder(c echo.Context) error { return c.JSON(http.StatusGone, map[string]string{"error": "Ebook folders are deprecated. Please use library system instead."}) } type UpdateThemeRequest struct { Theme string `json:"theme" validate:"required"` } // UpdateTheme handles PUT /api/auth/theme func (h *AuthHandler) UpdateTheme(c echo.Context) error { userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } 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: pgtype.UUID{Bytes: userUUID, Valid: true}, 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 { userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } 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 && uuid.UUID(existingUser.ID.Bytes) != userUUID { return c.JSON(http.StatusConflict, map[string]string{"error": "username already taken"}) } // Update username err = h.db.UpdateUsername(c.Request().Context(), database.UpdateUsernameParams{ ID: pgtype.UUID{Bytes: userUUID, Valid: true}, 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 { userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } 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 && uuid.UUID(existingUser.ID.Bytes) != userUUID { return c.JSON(http.StatusConflict, map[string]string{"error": "email already taken"}) } // Update email err = h.db.UpdateEmail(c.Request().Context(), database.UpdateEmailParams{ ID: pgtype.UUID{Bytes: userUUID, Valid: true}, 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,min=6"` ConfirmPassword string `json:"confirm_password" validate:"required"` } // UpdatePassword handles PUT /api/auth/password func (h *AuthHandler) UpdatePassword(c echo.Context) error { userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } 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(), pgtype.UUID{Bytes: userUUID, 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": "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: pgtype.UUID{Bytes: userUUID, Valid: true}, 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 { // Get target user ID from query parameter (for admin override) or use current user targetUserID := c.QueryParam("user_id") userID := c.Get("user_id").(string) // If admin override is used, validate admin and use target if targetUserID != "" { // Admin override mode - check if current user is admin userRole := c.Get("user_role").(string) if userRole != "admin" { return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"}) } userID = targetUserID } userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user 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 userUUIDStr := uuid.UUID(user.ID.Bytes).String() if userUUIDStr == userID { 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(), pgtype.UUID{Bytes: userUUID, Valid: true}) 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 != "" && userID != c.Get("user_id").(string) { message = "user account deleted successfully" } else { message = "account deleted successfully" } return c.JSON(http.StatusOK, map[string]string{"message": message}) } type UpdateScanSettingsRequest struct { ScanFrequencyMinutes int32 `json:"scan_frequency_minutes" validate:"required,min=15,max=1440"` AutoScanEnabled bool `json:"auto_scan_enabled"` } // UpdateScanSettings handles PUT /api/library/scan-settings func (h *AuthHandler) UpdateScanSettings(c echo.Context) error { userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } var req UpdateScanSettingsRequest 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.UpdateScanSettings(c.Request().Context(), database.UpdateScanSettingsParams{ ID: pgtype.UUID{Bytes: userUUID, Valid: true}, ScanFrequencyMinutes: pgtype.Int4{Int32: req.ScanFrequencyMinutes, Valid: true}, AutoScanEnabled: pgtype.Bool{Bool: req.AutoScanEnabled, Valid: true}, }) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]string{"message": "scan settings updated successfully"}) } // GetScanSettings handles GET /api/library/scan-settings func (h *AuthHandler) GetScanSettings(c echo.Context) error { userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } settings, err := h.db.GetScanSettings(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true}) if err != nil { if err == pgx.ErrNoRows { // If no settings found, return defaults return c.JSON(http.StatusOK, map[string]interface{}{ "scan_frequency_minutes": 60, "auto_scan_enabled": true, }) } return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]interface{}{ "scan_frequency_minutes": settings.ScanFrequencyMinutes.Int32, "auto_scan_enabled": settings.AutoScanEnabled.Bool, }) } // 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{ "user_id": userID, "user_role": userRole, "user_email": userEmail, "user_username": userUsername, "exp": time.Now().Add(24 * time.Hour).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") }