diff --git a/api/paths/users-id.yaml b/api/paths/users-id.yaml index 0acdb81..06f4a19 100644 --- a/api/paths/users-id.yaml +++ b/api/paths/users-id.yaml @@ -24,3 +24,79 @@ get: description: Request params are not correct '500': description: Unknown server error + +patch: + summary: Partially update a user account + description: | + Update selected user profile fields (excluding password). + Password updates must be done via the dedicated auth-service (`/auth/`). + Fields not provided in the request body remain unchanged. + operationId: updateUser + parameters: + - name: user_id + in: path + required: true + schema: + type: integer + format: int64 + description: User ID (primary key) + example: 123 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + avatar_id: + type: integer + format: int64 + nullable: true + description: ID of the user avatar (references `images.id`); set to `null` to remove avatar + example: 42 + mail: + type: string + format: email + pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' + description: User email (must be unique and valid) + example: john.doe.updated@example.com + nickname: + type: string + pattern: '^[a-zA-Z0-9_-]{3,16}$' + description: Username (alphanumeric + `_` or `-`, 3–16 chars) + maxLength: 16 + minLength: 3 + example: john_doe_43 + disp_name: + type: string + description: Display name + maxLength: 32 + example: John Smith + user_desc: + type: string + description: User description / bio + maxLength: 512 + example: Just a curious developer. + additionalProperties: false + description: Only provided fields are updated. Omitted fields remain unchanged. + responses: + '200': + description: User updated successfully. Returns updated user representation (excluding sensitive fields). + content: + application/json: + schema: + $ref: '../schemas/User.yaml' + '400': + description: Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON) + '401': + description: Unauthorized — missing or invalid authentication token + '403': + description: Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights) + '404': + description: User not found + '409': + description: Conflict — e.g., requested `nickname` or `mail` already taken by another user + '422': + description: Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`) + '500': + description: Unknown server error diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index d3848f4..781911f 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -33,7 +33,7 @@ func mapUser(u sqlc.GetUserByIDRow) oapi.User { CreationDate: &u.CreationDate, DispName: u.DispName, Id: &u.ID, - Mail: (*types.Email)(u.Mail), + Mail: StringToEmail(u.Mail), Nickname: u.Nickname, UserDesc: u.UserDesc, } @@ -270,3 +270,49 @@ func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersU return oapi.GetUsersUserIdTitles200JSONResponse{Cursor: new_cursor, Data: oapi_usertitles}, nil } + +func EmailToStringPtr(e *types.Email) *string { + if e == nil { + return nil + } + s := string(*e) + return &s +} + +func StringToEmail(e *string) *types.Email { + if e == nil { + return nil + } + s := types.Email(*e) + return &s +} + +// UpdateUser implements oapi.StrictServerInterface. +func (s Server) UpdateUser(ctx context.Context, request oapi.UpdateUserRequestObject) (oapi.UpdateUserResponseObject, error) { + + params := sqlc.UpdateUserParams{ + AvatarID: request.Body.AvatarId, + DispName: request.Body.DispName, + UserDesc: request.Body.UserDesc, + Mail: EmailToStringPtr(request.Body.Mail), + UserID: request.UserId, + } + + user, err := s.db.UpdateUser(ctx, params) + if err != nil { + log.Errorf("%v", err) + return oapi.UpdateUser500Response{}, nil + } + + oapi_user := oapi.User{ // maybe its possible to make one sqlc type and use one map func iinstead of this shit + AvatarId: user.AvatarID, + CreationDate: &user.CreationDate, + DispName: user.DispName, + Id: &user.ID, + Mail: StringToEmail(user.Mail), + Nickname: user.Nickname, + UserDesc: user.UserDesc, + } + + return oapi.UpdateUser200JSONResponse(oapi_user), nil +} diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index dc81da9..0bf1b86 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -58,15 +58,15 @@ RETURNING id, tag_names; -- VALUES ($1, $2, $3, $4, $5, $6, $7) -- RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; --- -- name: UpdateUser :one --- UPDATE users --- SET --- avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id), --- disp_name = COALESCE(sqlc.narg('disp_name'), disp_name), --- user_desc = COALESCE(sqlc.narg('user_desc'), user_desc), --- passhash = COALESCE(sqlc.narg('passhash'), passhash) --- WHERE user_id = sqlc.arg('user_id') --- RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; +-- name: UpdateUser :one +UPDATE users +SET + avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id), + disp_name = COALESCE(sqlc.narg('disp_name'), disp_name), + user_desc = COALESCE(sqlc.narg('user_desc'), user_desc), + mail = COALESCE(sqlc.narg('mail'), mail) +WHERE id = sqlc.arg('user_id') +RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail; -- -- name: DeleteUser :exec -- DELETE FROM users diff --git a/sql/queries.sql.go b/sql/queries.sql.go index 2df4da8..cac5543 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -114,9 +114,6 @@ func (q *Queries) GetStudioByID(ctx context.Context, studioID int64) (Studio, er const getTitleByID = `-- name: GetTitleByID :one - - - SELECT t.id, t.title_names, t.studio_id, t.poster_id, t.title_status, t.rating, t.rating_count, t.release_year, t.release_season, t.season, t.episodes_aired, t.episodes_all, t.episodes_len, i.storage_type::text as title_storage_type, @@ -167,26 +164,6 @@ type GetTitleByIDRow struct { StudioImagePath *string `json:"studio_image_path"` } -// -- name: ListUsers :many -// SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date -// FROM users -// ORDER BY user_id -// LIMIT $1 OFFSET $2; -// -- name: CreateUser :one -// INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date) -// VALUES ($1, $2, $3, $4, $5, $6, $7) -// RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; -// -- name: UpdateUser :one -// UPDATE users -// SET -// -// avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id), -// disp_name = COALESCE(sqlc.narg('disp_name'), disp_name), -// user_desc = COALESCE(sqlc.narg('user_desc'), user_desc), -// passhash = COALESCE(sqlc.narg('passhash'), passhash) -// -// WHERE user_id = sqlc.arg('user_id') -// RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; // -- name: DeleteUser :exec // DELETE FROM users // WHERE user_id = $1; @@ -784,3 +761,64 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara } return items, nil } + +const updateUser = `-- name: UpdateUser :one + + +UPDATE users +SET + avatar_id = COALESCE($1, avatar_id), + disp_name = COALESCE($2, disp_name), + user_desc = COALESCE($3, user_desc), + mail = COALESCE($4, mail) +WHERE id = $5 +RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail +` + +type UpdateUserParams struct { + AvatarID *int64 `json:"avatar_id"` + DispName *string `json:"disp_name"` + UserDesc *string `json:"user_desc"` + Mail *string `json:"mail"` + UserID int64 `json:"user_id"` +} + +type UpdateUserRow struct { + ID int64 `json:"id"` + AvatarID *int64 `json:"avatar_id"` + Nickname string `json:"nickname"` + DispName *string `json:"disp_name"` + UserDesc *string `json:"user_desc"` + CreationDate time.Time `json:"creation_date"` + Mail *string `json:"mail"` +} + +// -- name: ListUsers :many +// SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date +// FROM users +// ORDER BY user_id +// LIMIT $1 OFFSET $2; +// -- name: CreateUser :one +// INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date) +// VALUES ($1, $2, $3, $4, $5, $6, $7) +// RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) { + row := q.db.QueryRow(ctx, updateUser, + arg.AvatarID, + arg.DispName, + arg.UserDesc, + arg.Mail, + arg.UserID, + ) + var i UpdateUserRow + err := row.Scan( + &i.ID, + &i.AvatarID, + &i.Nickname, + &i.DispName, + &i.UserDesc, + &i.CreationDate, + &i.Mail, + ) + return i, err +}