From 382a2369c5bdfb6819c82979969288d47ef80c05 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 23 Jan 2026 21:27:10 -0500 Subject: [PATCH] Fix auth handler: remove duplicate validation, fix first_name/last_name type handling --- internal/handlers/auth.go | 127 +++++++++++++++++++++++++++----------- 1 file changed, 91 insertions(+), 36 deletions(-) diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index f9b677d..c7feb38 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -6,7 +6,8 @@ import ( "net/http" "time" - jwtgo "github.com/golang-jwt/jwt" + jwt "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" @@ -26,9 +27,11 @@ func NewAuthHandler(db *database.Queries, jwtSecret string) *AuthHandler { } 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"` + 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 { @@ -42,9 +45,16 @@ type AuthResponse struct { } type UserProfile struct { - ID string `json:"id"` - Email string `json:"email"` - Username string `json:"username"` + 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 @@ -53,6 +63,8 @@ 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") if email == "" || username == "" || password == "" { // Fallback to JSON binding @@ -72,15 +84,11 @@ func (h *AuthHandler) Register(c echo.Context) error { email = req.Email username = req.Username password = req.Password + firstName = req.FirstName + lastName = req.LastName } - req := RegisterRequest{Email: email, Username: username, 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()}) - } + 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, `
`+err.Error()+`
`) @@ -117,6 +125,8 @@ func (h *AuthHandler) Register(c echo.Context) error { 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" { @@ -198,12 +208,6 @@ func (h *AuthHandler) Login(c echo.Context) error { } return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } - 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()}) - } // Get user by email or username (includes password hash for verification) user, err := h.db.GetUserForLogin(c.Request().Context(), req.Login) @@ -240,16 +244,26 @@ localStorage.setItem('token', '%s'); localStorage.setItem('user', JSON.stringify(%s)); document.cookie = 'token=%s; path=/; max-age=86400'; window.location.href = '/'; -`, token, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username), token) +`, 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, + ID: uuid.UUID(user.ID.Bytes).String(), + Email: user.Email, + Username: user.Username, + FirstName: firstName, + LastName: lastName, }, }) } @@ -267,13 +281,48 @@ func (h *AuthHandler) GetProfile(c echo.Context) error { 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, + 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()) @@ -328,6 +377,10 @@ type EbookFolderResponse struct { 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) @@ -386,7 +439,7 @@ func (h *AuthHandler) GetEbookFolders(c echo.Context) error { return c.JSON(http.StatusOK, response) } -// DeleteEbookFolder handles DELETE /api/auth/ebook-folders/:folderPath +// 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) @@ -394,20 +447,23 @@ func (h *AuthHandler) DeleteEbookFolder(c echo.Context) error { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } - folderPath := c.Param("folderPath") - if folderPath == "" { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "folder path is required"}) + 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()}) } err = h.db.DeleteUserEbookFolder(c.Request().Context(), database.DeleteUserEbookFolderParams{ UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, - FolderPath: folderPath, + FolderPath: req.FolderPath, }) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } - return c.JSON(http.StatusOK, map[string]string{"message": "ebook folder removed"}) + return c.JSON(http.StatusOK, map[string]string{"message": "ebook folder deleted successfully"}) } type UpdateThemeRequest struct { @@ -647,12 +703,11 @@ func (h *AuthHandler) GetScanSettings(c echo.Context) error { } func (h *AuthHandler) generateJWT(userID string) (string, error) { - claims := jwtgo.MapClaims{ + claims := jwt.MapClaims{ "user_id": userID, "exp": time.Now().Add(24 * time.Hour).Unix(), "iat": time.Now().Unix(), } - - token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, claims) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(h.jwtKey) }