diff --git a/server/internal/data/users.sql.go b/server/internal/data/users.sql.go index e9aef21..9b5455e 100644 --- a/server/internal/data/users.sql.go +++ b/server/internal/data/users.sql.go @@ -178,10 +178,11 @@ func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]User, e return items, nil } -const updatePassword = `-- name: UpdatePassword :exec +const updatePassword = `-- name: UpdatePassword :one UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1 +RETURNING id, username, password_hash, is_admin, created_at, updated_at ` type UpdatePasswordParams struct { @@ -189,7 +190,16 @@ type UpdatePasswordParams struct { PasswordHash string `json:"password_hash"` } -func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error { - _, err := q.db.Exec(ctx, updatePassword, arg.ID, arg.PasswordHash) - return err +func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) (User, error) { + row := q.db.QueryRow(ctx, updatePassword, arg.ID, arg.PasswordHash) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.PasswordHash, + &i.IsAdmin, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err } diff --git a/server/internal/service/auth.go b/server/internal/service/auth.go index 2a53392..5e3a1ee 100644 --- a/server/internal/service/auth.go +++ b/server/internal/service/auth.go @@ -71,7 +71,7 @@ type UserStore interface { ListUsers(ctx context.Context, arg data.ListUsersParams) ([]data.User, error) GetUserByID(ctx context.Context, id uuid.UUID) (data.User, error) GetUserByUsername(ctx context.Context, username string) (data.User, error) - UpdatePassword(ctx context.Context, arg data.UpdatePasswordParams) error + UpdatePassword(ctx context.Context, arg data.UpdatePasswordParams) (data.User, error) DeleteUser(ctx context.Context, id uuid.UUID) error RevokeAllUserRefreshTokens(ctx context.Context, id uuid.UUID) error } @@ -271,7 +271,7 @@ func (rs authResource) Get(w http.ResponseWriter, r *http.Request) { // Handler for updating the current user's password. Performs the same password strength checks as // the registration handler (`rs.Create`) and revokes any existing refresh tokens the user has -// stored in the database. +// stored in the database. The new access token and the updated user object's DTO will be returned. func (rs authResource) UpdatePassword(w http.ResponseWriter, r *http.Request) { type request struct { OldPassword string `json:"old_password"` @@ -306,19 +306,51 @@ func (rs authResource) UpdatePassword(w http.ResponseWriter, r *http.Request) { return } - if err := rs.Users.UpdatePassword(r.Context(), data.UpdatePasswordParams{ + nUSer, err := rs.Users.UpdatePassword(r.Context(), data.UpdatePasswordParams{ ID: user.ID, PasswordHash: string(hashedPassword), - }); err != nil { + }) + if err != nil { respondError(w, http.StatusInternalServerError, "Failed to update password") return } + // Revoke all old tokens before generating a new one for this session if err := rs.Users.RevokeAllUserRefreshTokens(r.Context(), user.ID); err != nil { log.Error().Msgf("Failed to revoke refresh tokens: %s", err) } - w.WriteHeader(http.StatusNoContent) + // Generate a new pair (access & refresh tokens) + tokenPair, err := rs.GenerateTokenPair(r.Context(), user.ID, user.IsAdmin) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to generate tokens") + return + } + + // Set refresh token into a httpOnly cookie + http.SetCookie(w, &http.Cookie{ + Name: "notatest.refresh_token", + Value: tokenPair.RefreshToken, + Path: "/api/auth/cookie", + MaxAge: int(refreshTokenDuration.Seconds()), + HttpOnly: true, + Secure: rs.Config.IsProd, + SameSite: http.SameSiteStrictMode, + }) + + response := map[string]any{ + "access_token": tokenPair.AccessToken, + "user": userResponse{ + ID: nUSer.ID, + Username: nUSer.Username, + IsAdmin: nUSer.IsAdmin, + CreatedAt: nUSer.CreatedAt, + UpdatedAt: nUSer.UpdatedAt, + }, + } + + // Return the new access token and the updated user object + respondJSON(w, http.StatusOK, response) } // Handler for hard deleting the current user. Requires the user's password as JSON input as a precaution. diff --git a/server/sql/queries/users.sql b/server/sql/queries/users.sql index 5fc227f..a08a068 100644 --- a/server/sql/queries/users.sql +++ b/server/sql/queries/users.sql @@ -24,10 +24,11 @@ WHERE id = $1 LIMIT 1; SELECT * FROM users WHERE username = $1 LIMIT 1; --- name: UpdatePassword :exec +-- name: UpdatePassword :one UPDATE users SET password_hash = $2, updated_at = NOW() -WHERE id = $1; +WHERE id = $1 +RETURNING *; -- name: DeleteUser :exec DELETE FROM users