feat!: return user dto & create new rt on pw update

This commit is contained in:
ae 2025-04-12 23:48:15 +03:00
parent 298aca465e
commit 0f3fef20e3
Signed by: ae
GPG Key ID: 995EFD5C1B532B3E
3 changed files with 54 additions and 11 deletions

View File

@ -178,10 +178,11 @@ func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]User, e
return items, nil return items, nil
} }
const updatePassword = `-- name: UpdatePassword :exec const updatePassword = `-- name: UpdatePassword :one
UPDATE users UPDATE users
SET password_hash = $2, updated_at = NOW() SET password_hash = $2, updated_at = NOW()
WHERE id = $1 WHERE id = $1
RETURNING id, username, password_hash, is_admin, created_at, updated_at
` `
type UpdatePasswordParams struct { type UpdatePasswordParams struct {
@ -189,7 +190,16 @@ type UpdatePasswordParams struct {
PasswordHash string `json:"password_hash"` PasswordHash string `json:"password_hash"`
} }
func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error { func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) (User, error) {
_, err := q.db.Exec(ctx, updatePassword, arg.ID, arg.PasswordHash) row := q.db.QueryRow(ctx, updatePassword, arg.ID, arg.PasswordHash)
return err var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.PasswordHash,
&i.IsAdmin,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
} }

View File

@ -71,7 +71,7 @@ type UserStore interface {
ListUsers(ctx context.Context, arg data.ListUsersParams) ([]data.User, error) ListUsers(ctx context.Context, arg data.ListUsersParams) ([]data.User, error)
GetUserByID(ctx context.Context, id uuid.UUID) (data.User, error) GetUserByID(ctx context.Context, id uuid.UUID) (data.User, error)
GetUserByUsername(ctx context.Context, username string) (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 DeleteUser(ctx context.Context, id uuid.UUID) error
RevokeAllUserRefreshTokens(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 // 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 // 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) { func (rs authResource) UpdatePassword(w http.ResponseWriter, r *http.Request) {
type request struct { type request struct {
OldPassword string `json:"old_password"` OldPassword string `json:"old_password"`
@ -306,19 +306,51 @@ func (rs authResource) UpdatePassword(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := rs.Users.UpdatePassword(r.Context(), data.UpdatePasswordParams{ nUSer, err := rs.Users.UpdatePassword(r.Context(), data.UpdatePasswordParams{
ID: user.ID, ID: user.ID,
PasswordHash: string(hashedPassword), PasswordHash: string(hashedPassword),
}); err != nil { })
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to update password") respondError(w, http.StatusInternalServerError, "Failed to update password")
return return
} }
// Revoke all old tokens before generating a new one for this session
if err := rs.Users.RevokeAllUserRefreshTokens(r.Context(), user.ID); err != nil { if err := rs.Users.RevokeAllUserRefreshTokens(r.Context(), user.ID); err != nil {
log.Error().Msgf("Failed to revoke refresh tokens: %s", err) 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. // Handler for hard deleting the current user. Requires the user's password as JSON input as a precaution.

View File

@ -24,10 +24,11 @@ WHERE id = $1 LIMIT 1;
SELECT * FROM users SELECT * FROM users
WHERE username = $1 LIMIT 1; WHERE username = $1 LIMIT 1;
-- name: UpdatePassword :exec -- name: UpdatePassword :one
UPDATE users UPDATE users
SET password_hash = $2, updated_at = NOW() SET password_hash = $2, updated_at = NOW()
WHERE id = $1; WHERE id = $1
RETURNING *;
-- name: DeleteUser :exec -- name: DeleteUser :exec
DELETE FROM users DELETE FROM users