diff --git a/doc/api/users.md b/doc/api/users.md index c7a47298c9373a7ca8585b440b45f03dee820754..9302c541754496685761ed17d55b8003e78f78b9 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -2543,3 +2543,43 @@ Example response: "token_expires_at": null } ``` + +## Upload a current user avatar + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148130) in GitLab 17.0. + +Upload an avatar to current user. + +```plaintext +PUT /user/avatar +``` + +| Attribute | Type | Required | Description | +|-----------|-------------------|----------|-------------------------------------------------------------------------------------------------------------| +| `avatar` | string | Yes | The file to be uploaded. The ideal image size is 192 x 192 pixels. The maximum file size allowed is 200 KiB. | + +To upload an avatar from your file system, use the `--form` argument. This causes +cURL to post data using the header `Content-Type: multipart/form-data`. The +`file=` parameter must point to an image file on your file system and be +preceded by `@`. For example: + +Example request: + +```shell +curl --request PUT --header "Bearer: " \ + --form "avatar=@avatar.png" \ + --url "https://gitlab.example.com/api/v4/user/avatar" +``` + +Returned object: + +Returns `400 Bad Request` for file sizes greater than 200 KiB. + +If successful, returns [`200`](rest/index.md#status-codes) and the following +response attributes: + +```json +{ + "avatar_url": "http://gdk.test:3000/uploads/-/system/user/avatar/76/avatar.png", +} +``` diff --git a/lib/api/users.rb b/lib/api/users.rb index 8b290875870f9b6589cd2e45d3dd68120b70071f..315c8556b8107d057f959f61f830c65fcfcbb5be 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -1394,6 +1394,27 @@ def set_user_status(include_missing_params:) present current_user.status || {}, with: Entities::UserStatus end + desc 'Set the avatar of the current user' do + success Entities::Avatar + detail 'This feature was introduced in GitLab 17.0.' + end + params do + requires :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'The avatar file (generated by Multipart middleware)', documentation: { type: 'file' } + end + put "avatar", feature_category: :user_profile do + update_params = { + avatar: declared_params[:avatar], + user: current_user + } + result = ::Users::UpdateService.new(current_user, update_params).execute + + if result[:status] == :success + present current_user, with: Entities::Avatar + else + render_api_error!(result[:message], result[:reason] || :bad_request) + end + end + resource :personal_access_tokens do desc 'Create a personal access token with limited scopes for the currently authenticated user' do detail 'This feature was introduced in GitLab 16.5' diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index d14e672cd411e9b03584922d23843e5826a29d9f..0c930396c9c54d7cf7331a0a10f0859d3ed4b5be 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -4607,6 +4607,36 @@ def update_password(user, admin, password = User.random_password) end end + describe 'PUT /user/avatar' do + let(:path) { "/user/avatar" } + + it "returns 200 OK on success" do + workhorse_form_with_file( + api(path, user), + method: :put, + file_key: :avatar, + params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } + ) + + user.reload + expect(user.avatar).to be_present + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['avatar_url']).to include(user.avatar_path) + end + + it "returns 400 when avatar file size over 200 KiB" do + workhorse_form_with_file( + api(path, user), + method: :put, + file_key: :avatar, + params: { avatar: fixture_file_upload('spec/fixtures/big-image.png', 'image/png') } + ) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include("Avatar is too big (should be at most 200 KiB)") + end + end + describe 'POST /users/:user_id/personal_access_tokens' do let(:name) { 'new pat' } let(:expires_at) { 3.days.from_now.to_date.to_s } diff --git a/workhorse/cmd/gitlab-workhorse/upload_test.go b/workhorse/cmd/gitlab-workhorse/upload_test.go index a6070cfeea656500fdd000e19f08d3200f87e412..4e4312f809e51f4586ede1e93af5b82df1450877 100644 --- a/workhorse/cmd/gitlab-workhorse/upload_test.go +++ b/workhorse/cmd/gitlab-workhorse/upload_test.go @@ -153,6 +153,7 @@ func TestAcceleratedUpload(t *testing.T) { {"PUT", `/api/v4/groups/group%2Fsubgroup`, false}, {"POST", `/api/v4/groups/1/wikis/attachments`, false}, {"POST", `/api/v4/groups/my%2Fsubgroup/wikis/attachments`, false}, + {"PUT", `/api/v4/user/avatar`, false}, {"POST", `/api/v4/users`, false}, {"PUT", `/api/v4/users/42`, false}, {"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true}, diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go index bc0640029bf70557298ee479a2f3ad6f70068615..92ed18aace65f11cce46c9db237d0d2dae06f7ea 100644 --- a/workhorse/internal/upstream/routes.go +++ b/workhorse/internal/upstream/routes.go @@ -343,6 +343,7 @@ func configureRoutes(u *upstream) { u.route("PUT", apiPattern+`v4/groups/[^/]+\z`, tempfileMultipartProxy), // User Avatar + u.route("PUT", apiPattern+`v4/user/avatar\z`, tempfileMultipartProxy), u.route("POST", apiPattern+`v4/users\z`, tempfileMultipartProxy), u.route("PUT", apiPattern+`v4/users/[0-9]+\z`, tempfileMultipartProxy),