Compare commits

..

No commits in common. "5b633498cbaba268ed97703c07f203034a1fd0b4" and "298aca465ecdd97c47bcc391b53e181d9c9cd00a" have entirely different histories.

4 changed files with 12 additions and 55 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) - `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 - `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` -> Cookie with a new refresh token and response with access token and updated user data - `PUT /auth/owner/`: Update password of the current user with `old_password` and `new_password` -> HTTP 204 response
- `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 - `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 - `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 - `DELETE /auth/admin/{userID}`: As an administrator, delete a specific user -> HTTP 204 response

View File

@ -178,11 +178,10 @@ func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]User, e
return items, nil return items, nil
} }
const updatePassword = `-- name: UpdatePassword :one const updatePassword = `-- name: UpdatePassword :exec
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 {
@ -190,16 +189,7 @@ type UpdatePasswordParams struct {
PasswordHash string `json:"password_hash"` PasswordHash string `json:"password_hash"`
} }
func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) (User, error) { func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error {
row := q.db.QueryRow(ctx, updatePassword, arg.ID, arg.PasswordHash) _, err := q.db.Exec(ctx, updatePassword, arg.ID, arg.PasswordHash)
var i User return err
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) (data.User, error) UpdatePassword(ctx context.Context, arg data.UpdatePasswordParams) 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. The new access token and the updated user object's DTO will be returned. // stored in the database.
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,51 +306,19 @@ func (rs authResource) UpdatePassword(w http.ResponseWriter, r *http.Request) {
return return
} }
nUSer, err := rs.Users.UpdatePassword(r.Context(), data.UpdatePasswordParams{ if 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)
} }
// Generate a new pair (access & refresh tokens) w.WriteHeader(http.StatusNoContent)
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,11 +24,10 @@ WHERE id = $1 LIMIT 1;
SELECT * FROM users SELECT * FROM users
WHERE username = $1 LIMIT 1; WHERE username = $1 LIMIT 1;
-- name: UpdatePassword :one -- name: UpdatePassword :exec
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