Compare commits

..

2 Commits

Author SHA1 Message Date
ae
5b633498cb
docs: pw. update response 2025-04-12 23:49:09 +03:00
ae
0f3fef20e3
feat!: return user dto & create new rt on pw update 2025-04-12 23:48:15 +03:00
4 changed files with 55 additions and 12 deletions

View File

@ -13,7 +13,7 @@ Endpoints protected with `requireAccessToken` middleware:
- `GET /auth/me`: Get own user's data -> `userResponse` DTO (user ID, username, admin status, and timestamps of creation and last update)
- `POST /auth/logout`: Logout the current user -> Cookie that replaces the refresh token with one that expires immediately
- `PUT /auth/owner/`: Update password of the current user with `old_password` and `new_password` -> HTTP 204 response
- `PUT /auth/owner/`: Update password of the current user with `old_password` and `new_password` -> Cookie with a new refresh token and response with access token and updated user data
- `DELETE /auth/owner/`: Delete the current user (as the owner) with `password` -> HTTP 204 response and cookie that replaces the refresh token with one that expires immediately
- `GET /auth/admin/all`: As an administrator, list all users stored in the system (adjustable with pagination URL parameters) -> Array of `userResponse` DTOs
- `DELETE /auth/admin/{userID}`: As an administrator, delete a specific user -> HTTP 204 response

View File

@ -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
}

View File

@ -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.

View File

@ -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