Files
bookhoard/internal/handlers/auth.go
T
john-okeefe 08e80ae84b refactor: reorganize project structure and update configurations
- Move migrations/ to database/schema/ for clarity on database schema definitions
- Move sqlc.yaml to internal/database/ to group with database code
- Move static/ to cmd/server/static/ to co-locate with server
- Update all configuration files and documentation
- Follow Go project conventions for better organization
2026-01-24 23:40:31 -05:00

763 lines
26 KiB
Go

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"`
}
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"`
}
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")
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, `<div class="text-red-500">Invalid request</div>`)
}
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, `<div class="text-red-500">`+err.Error()+`</div>`)
}
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
}
req := RegisterRequest{Email: email, Username: username, Password: password, FirstName: firstName, LastName: lastName}
if err := c.Validate(&req); err != nil {
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
}
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, `<div class="text-red-500">Email already exists</div>`)
}
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, `<div class="text-red-500">Username already exists</div>`)
}
return c.JSON(http.StatusConflict, map[string]string{"error": "username already exists"})
}
// 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, `<div class="text-red-500">Failed to hash password</div>`)
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
}
// 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 != ""},
})
if err != nil {
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">`+err.Error()+`</div>`)
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
// Generate JWT
token, err := h.generateJWT(uuid.UUID(user.ID.Bytes).String())
if err != nil {
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to generate token</div>`)
}
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(`<div class="text-green-500">Registration successful! Redirecting...</div>
<script>
localStorage.setItem('token', '%s');
localStorage.setItem('user', JSON.stringify(%s));
document.cookie = 'token=%s; path=/; max-age=86400';
window.location.href = '/';
</script>`, 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)
}
return c.JSON(http.StatusCreated, AuthResponse{
Token: token,
User: UserProfile{
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Username: user.Username,
},
})
}
// 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, `<div class="text-red-500">Invalid request</div>`)
}
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, `<div class="text-red-500">`+err.Error()+`</div>`)
}
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, `<div class="text-red-500">`+err.Error()+`</div>`)
}
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, `<div class="text-red-500">Invalid credentials</div>`)
}
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, `<div class="text-red-500">Invalid credentials</div>`)
}
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
}
// Generate JWT
token, err := h.generateJWT(uuid.UUID(user.ID.Bytes).String())
if err != nil {
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to generate token</div>`)
}
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(`<div class="text-green-500">Login successful! Redirecting...</div>
<script>
localStorage.setItem('token', '%s');
localStorage.setItem('user', JSON.stringify(%s));
document.cookie = 'token=%s; path=/; max-age=86400';
window.location.href = '/';
</script>`, 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,
},
})
}
// 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 {
return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
}
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,
})
}
// 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/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"`
Theme string `json:"theme"`
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
}
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,
Theme: theme,
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
func (h *AuthHandler) AddEbookFolder(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 AddEbookFolderRequest
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()})
}
// Normalize the folder path before storing
normalizedPath := normalizePath(req.FolderPath)
folder, err := h.db.AddUserEbookFolder(c.Request().Context(), database.AddUserEbookFolderParams{
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
FolderPath: normalizedPath,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, EbookFolderResponse{
ID: uuid.UUID(folder.ID.Bytes).String(),
UserID: uuid.UUID(folder.UserID.Bytes).String(),
FolderPath: folder.FolderPath,
CreatedAt: folder.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
})
}
// GetEbookFolders handles GET /api/auth/ebook-folders
func (h *AuthHandler) GetEbookFolders(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"})
}
folders, err := h.db.GetUserEbookFolders(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
fmt.Printf("GetEbookFolders: user_id='%s'\n", userID)
for _, folder := range folders {
fmt.Printf(" Folder in DB: id='%s', path='%s'\n",
uuid.UUID(folder.ID.Bytes).String(), folder.FolderPath)
}
var response []EbookFolderResponse
for _, folder := range folders {
response = append(response, EbookFolderResponse{
ID: uuid.UUID(folder.ID.Bytes).String(),
UserID: uuid.UUID(folder.UserID.Bytes).String(),
FolderPath: folder.FolderPath,
CreatedAt: folder.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
})
}
return c.JSON(http.StatusOK, response)
}
// DeleteEbookFolder handles DELETE /api/auth/ebook-folders
func (h *AuthHandler) DeleteEbookFolder(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 DeleteEbookFolderRequest
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()})
}
// Normalize the folder path before deletion
normalizedPath := normalizePath(req.FolderPath)
// Debug logging - remove in production
fmt.Printf("DeleteEbookFolder: original path='%s', normalized path='%s', user_id='%s'\n",
req.FolderPath, normalizedPath, userID)
deletedFolder, err := h.db.DeleteUserEbookFolder(c.Request().Context(), database.DeleteUserEbookFolderParams{
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
FolderPath: normalizedPath,
})
if err != nil {
fmt.Printf("DeleteEbookFolder failed: %v\n", err)
if err == pgx.ErrNoRows {
return c.JSON(http.StatusNotFound, map[string]string{"error": "ebook folder not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
fmt.Printf("DeleteEbookFolder succeeded: deleted folder with path '%s'\n", deletedFolder.FolderPath)
return c.JSON(http.StatusOK, map[string]string{"message": "ebook folder deleted successfully"})
}
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/user/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/user/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/user/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 {
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/user/account
func (h *AuthHandler) DeleteAccount(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"})
}
// 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 {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"message": "account deleted successfully"})
}
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 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.StatusOK, map[string]interface{}{
"scan_frequency_minutes": settings.ScanFrequencyMinutes.Int32,
"auto_scan_enabled": settings.AutoScanEnabled.Bool,
})
}
func (h *AuthHandler) generateJWT(userID string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(h.jwtKey)
}