diff --git a/app/controllers/api/v0/profile_images_controller.rb b/app/controllers/api/v0/profile_images_controller.rb new file mode 100644 index 000000000..89be6a32c --- /dev/null +++ b/app/controllers/api/v0/profile_images_controller.rb @@ -0,0 +1,27 @@ +module Api + module V0 + class ProfileImagesController < ApiController + def show + not_found unless profile_image_owner + + @profile_image_owner = profile_image_owner + end + + private + + def profile_image_owner + user || organization + end + + def user + @user ||= User.registered.select(:id, :profile_image) + .find_by(username: params[:username]) + end + + def organization + @organization ||= Organization.select(:id, :profile_image) + .find_by(username: params[:username]) + end + end + end +end diff --git a/app/views/api/v0/profile_images/show.json.jbuilder b/app/views/api/v0/profile_images/show.json.jbuilder new file mode 100644 index 000000000..4be52b59e --- /dev/null +++ b/app/views/api/v0/profile_images/show.json.jbuilder @@ -0,0 +1,5 @@ +json.type_of "profile_image" + +json.image_of @profile_image_owner.class.name.downcase +json.profile_image Images::Profile.call(@profile_image_owner.profile_image_url, length: 640) +json.profile_image_90 Images::Profile.call(@profile_image_owner.profile_image_url, length: 90) diff --git a/config/routes.rb b/config/routes.rb index 12b58ba28..155757115 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -194,6 +194,8 @@ Rails.application.routes.draw do get :cache end end + + resources :profile_images, only: %i[show], param: :username end end diff --git a/docs/api_v0.yml b/docs/api_v0.yml index 164487c9f..549bc4339 100644 --- a/docs/api_v0.yml +++ b/docs/api_v0.yml @@ -17,7 +17,7 @@ info: Dates and date times, unless otherwise specified, must be in the [RFC 3339](https://tools.ietf.org/html/rfc3339) format. - version: "0.9.0" + version: "0.9.1" termsOfService: https://dev.to/terms contact: name: DEV Team @@ -1086,6 +1086,24 @@ components: user: $ref: "#/components/schemas/SharedUser" + ProfileImage: + description: Profile image + type: object + properties: + type_of: + type: string + enum: [profile_image] + image_of: + description: "Discriminates what is the type of the profile image owner (user or organization)" + type: string + enum: [user, organization] + profile_image: + type: string + description: Profile image (640x640) + profile_image_90: + type: string + description: Profile image (90x90) + examples: ErrorBadRequest: value: @@ -1642,6 +1660,13 @@ components: profile_image: "..." profile_image_90: "..." + ProfileImage: + value: + type_of: profile_image + image_of: user + profile_image: https://res.cloudinary.com/...jpeg + profile_image_90: https://res.cloudinary.com/...jpeg + tags: - name: articles description: Articles are all the posts users create on DEV @@ -1665,6 +1690,8 @@ tags: description: Video articles - name: webhooks description: Webhooks are HTTP endpoints registered to receive events + - name: profile images + description: User or organization profile images paths: /articles: @@ -3080,6 +3107,48 @@ paths: source: | curl -H "api-key: API_KEY" https://dev.to/api/users/me + /profile_images/{username}: + get: + operationId: getProfileImage + summary: User or organization profile picture + description: | + This endpoint allows the client to retrieve a user or organization + profile image information by its corresponding username + tags: + - profile images + parameters: + - name: username + in: path + required: true + description: Username of the user or organization + schema: + type: string + example: "diogoosorio" + responses: + "200": + description: The profile image + content: + application/json: + schema: + $ref: "#/components/schemas/ProfileImage" + examples: + profile-image-success: + $ref: "#/components/examples/ProfileImage" + "404": + description: Resource not found + content: + application/json: + schema: + $ref: "#/components/schemas/APIError" + examples: + profile-image-not-found: + $ref: "#/components/examples/ErrorNotFound" + x-code-samples: + - lang: Shell + label: curl + source: | + curl https://dev.to/api/profile_images/diogoosorio + /videos: get: operationId: getArticlesWithVideo diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 164836b20..0ec72a819 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -83,6 +83,13 @@ FactoryBot.define do after(:build) { |user| user.add_role(:banned) } end + trait :invited do + after(:build) do |user| + user.registered = false + user.registered_at = nil + end + end + trait :ignore_mailchimp_subscribe_callback do after(:build) do |user| user.define_singleton_method(:subscribe_to_mailchimp_newsletter) {} diff --git a/spec/requests/api/v0/profile_images_spec.rb b/spec/requests/api/v0/profile_images_spec.rb new file mode 100644 index 000000000..3fc7031f3 --- /dev/null +++ b/spec/requests/api/v0/profile_images_spec.rb @@ -0,0 +1,55 @@ +require "rails_helper" + +RSpec.describe "Api::V0::ProfileImages", type: :request do + describe "GET /api/profile_images/:username" do + it "returns 404 if the username is not taken" do + get api_profile_image_path("invalid-username") + + expect(response).to have_http_status(:not_found) + end + + context "when the username relates to an user" do + let(:user) { create(:user) } + + it "returns the user profile image information" do + get api_profile_image_path(user.username) + + expect(response.parsed_body).to eq( + "type_of" => "profile_image", + "image_of" => "user", + "profile_image" => profile_image(user.profile_image_url, 640), + "profile_image_90" => profile_image(user.profile_image_url, 90), + ) + end + end + + context "when the username relates to an invited user" do + let(:user) { create(:user, :invited) } + + it "returns a 404" do + get api_profile_image_path(user.username) + + expect(response).to have_http_status(:not_found) + end + end + + context "when the username relates to an organization" do + let(:organization) { create(:organization) } + + it "returns the organization's profile image information" do + get api_profile_image_path(organization.username) + + expect(response.parsed_body).to eq( + "type_of" => "profile_image", + "image_of" => "organization", + "profile_image" => profile_image(organization.profile_image_url, 640), + "profile_image_90" => profile_image(organization.profile_image_url, 90), + ) + end + end + end + + def profile_image(url, length) + Images::Profile.call(url, length: length) + end +end