diff --git a/changelogs/unreleased/45821-avatar_api.yml b/changelogs/unreleased/45821-avatar_api.yml new file mode 100644 index 00000000000..e16b28c36a2 --- /dev/null +++ b/changelogs/unreleased/45821-avatar_api.yml @@ -0,0 +1,5 @@ +--- +title: Add Avatar API +merge_request: 19121 +author: Imre Farkas +type: added diff --git a/doc/api/avatar.md b/doc/api/avatar.md new file mode 100644 index 00000000000..7faed893066 --- /dev/null +++ b/doc/api/avatar.md @@ -0,0 +1,33 @@ +# Avatar API + +> [Introduced][ce-19121] in GitLab 11.0 + +## Get a single avatar URL + +Get a single avatar URL for a given email addres. If user with matching public +email address is not found, results from external avatar services are returned. +This endpoint can be accessed without authentication. In case public visibility +is restricted, response will be `403 Forbidden` when unauthenticated. + +``` +GET /avatar?email=admin@example.com +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `email` | string | yes | Public email address of the user | +| `size` | integer | no | Single pixel dimension (since images are squares). Only used for avatar lookups at `Gravatar` or at the configured `Libravatar` server | + +```bash +curl https://gitlab.example.com/api/v4/avatar?email=admin@example.com +``` + +Example response: + +```json +{ + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" +} +``` + +[ce-19121]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19121 diff --git a/lib/api/api.rb b/lib/api/api.rb index 7ea575a9661..e2ad3c5f4e3 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -83,6 +83,7 @@ module API # Keep in alphabetical order mount ::API::AccessRequests mount ::API::Applications + mount ::API::Avatar mount ::API::AwardEmoji mount ::API::Badges mount ::API::Boards diff --git a/lib/api/avatar.rb b/lib/api/avatar.rb new file mode 100644 index 00000000000..70219bc8ea0 --- /dev/null +++ b/lib/api/avatar.rb @@ -0,0 +1,21 @@ +module API + class Avatar < Grape::API + resource :avatar do + desc 'Return avatar url for a user' do + success Entities::Avatar + end + params do + requires :email, type: String, desc: 'Public email address of the user' + optional :size, type: Integer, desc: 'Single pixel dimension for Gravatar images' + end + get do + forbidden!('Unauthorized access') unless can?(current_user, :read_users_list) + + user = User.find_by_public_email(params[:email]) + user ||= User.new(email: params[:email]) + + present user, with: Entities::Avatar, size: params[:size] + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index c76d3ff45d0..22afcb9edf2 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -694,6 +694,12 @@ module API expose :notes, using: Entities::Note end + class Avatar < Grape::Entity + expose :avatar_url do |avatarable, options| + avatarable.avatar_url(only_path: false, size: options[:size]) + end + end + class AwardEmoji < Grape::Entity expose :id expose :name diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb new file mode 100644 index 00000000000..26e0435a6d5 --- /dev/null +++ b/spec/requests/api/avatar_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper' + +describe API::Avatar do + let(:gravatar_service) { double('GravatarService') } + + describe 'GET /avatar' do + context 'avatar uploaded to GitLab' do + context 'user with matching public email address' do + let(:user) { create(:user, :with_avatar, email: 'public@example.com', public_email: 'public@example.com') } + + before do + user + end + + it 'returns the avatar url' do + get api('/avatar'), { email: 'public@example.com' } + + expect(response.status).to eq 200 + expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}") + end + end + + context 'no user with matching public email address' do + before do + expect(GravatarService).to receive(:new).and_return(gravatar_service) + expect(gravatar_service).to( + receive(:execute) + .with('private@example.com', nil, 2, { username: nil }) + .and_return('https://gravatar')) + end + + it 'returns the avatar url from Gravatar' do + get api('/avatar'), { email: 'private@example.com' } + + expect(response.status).to eq 200 + expect(json_response['avatar_url']).to eq('https://gravatar') + end + end + end + + context 'avatar uploaded to Gravatar' do + context 'user with matching public email address' do + let(:user) { create(:user, email: 'public@example.com', public_email: 'public@example.com') } + + before do + user + + expect(GravatarService).to receive(:new).and_return(gravatar_service) + expect(gravatar_service).to( + receive(:execute) + .with('public@example.com', nil, 2, { username: user.username }) + .and_return('https://gravatar')) + end + + it 'returns the avatar url from Gravatar' do + get api('/avatar'), { email: 'public@example.com' } + + expect(response.status).to eq 200 + expect(json_response['avatar_url']).to eq('https://gravatar') + end + end + + context 'no user with matching public email address' do + before do + expect(GravatarService).to receive(:new).and_return(gravatar_service) + expect(gravatar_service).to( + receive(:execute) + .with('private@example.com', nil, 2, { username: nil }) + .and_return('https://gravatar')) + end + + it 'returns the avatar url from Gravatar' do + get api('/avatar'), { email: 'private@example.com' } + + expect(response.status).to eq 200 + expect(json_response['avatar_url']).to eq('https://gravatar') + end + end + + context 'public visibility level restricted' do + let(:user) { create(:user, :with_avatar, email: 'public@example.com', public_email: 'public@example.com') } + + before do + user + + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + context 'when authenticated' do + it 'returns the avatar url' do + get api('/avatar', user), { email: 'public@example.com' } + + expect(response.status).to eq 200 + expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}") + end + end + + context 'when unauthenticated' do + it_behaves_like '403 response' do + let(:request) { get api('/avatar'), { email: 'public@example.com' } } + end + end + end + end + end +end