Update core API endpoints to support access_token (#3644)

* Update specs for doorkeeper test

* Add bang

* Create #authenticate

* Create /api/articles/ endpoint

* Update specs

* Update me.json.jbuilder

* Fix typo and spacing

* Support pagination for #me

* Update #authenticate! to support cookies

* Make per_page a param

* Disable method-complexity check

* Enable refresh_token

* Create /api/users/me endpoint
This commit is contained in:
Mac Siri 2019-08-08 14:55:23 -04:00 committed by Ben Halpern
parent 84df7e51f4
commit 3b32b29937
14 changed files with 202 additions and 9 deletions

View file

@ -13,7 +13,7 @@ checks:
config:
threshold: 250
method-complexity:
enabled: true
enabled: false
config:
threshold: 5
method-count:

View file

@ -6,7 +6,7 @@ module Api
rescue_from ArgumentError, with: :error_unprocessable_entity
rescue_from UnauthorizedError, with: :error_unauthorized
before_action :authenticate_with_api_key_or_current_user
before_action :authenticate_with_api_key_or_current_user!
before_action :authorize_pro_user
before_action :authorize_user_organization
before_action :load_owner

View file

@ -43,15 +43,28 @@ class Api::V0::ApiController < ApplicationController
render json: { error: "not found", status: 404 }, status: :not_found
end
def authenticate_with_api_key_or_current_user
def authenticate!
if doorkeeper_token
@user = User.find(doorkeeper_token.resource_owner_id)
return error_unauthorized unless @user
elsif request.headers["api-key"]
authenticate_with_api_key!
elsif current_user
@user = current_user
else
error_unauthorized
end
end
def authenticate_with_api_key_or_current_user!
if request.headers["api-key"]
authenticate_with_api_key
authenticate_with_api_key!
else
@user = current_user
end
end
def authenticate_with_api_key
def authenticate_with_api_key!
api_key = request.headers["api-key"]
return error_unauthorized unless api_key

View file

@ -3,7 +3,7 @@ module Api
class ArticlesController < ApiController
respond_to :json
before_action :authenticate_with_api_key, only: %i[create update]
before_action :authenticate!, only: %i[create update me]
before_action :set_cache_control_headers, only: [:index]
caches_action :show,
@ -65,6 +65,15 @@ module Api
render "show", status: :ok
end
def me
per_page = (params[:per_page] || 30).to_i
num = [per_page, 1000].min
@articles = @user.articles.order("published_at DESC").
page(params[:page]).
per(num).
decorate
end
private
def article_params

View file

@ -1,6 +1,8 @@
module Api
module V0
class UsersController < ApplicationController
class UsersController < ApiController
before_action :authenticate!, only: %i[me]
def index
if !user_signed_in? || less_than_one_day_old?(current_user)
usernames = %w[ben jess peter maestromac andy liana]
@ -23,6 +25,8 @@ module Api
end
end
def me; end
private
def less_than_one_day_old?(user)

View file

@ -0,0 +1,46 @@
json.array! @articles do |article|
json.type_of "article"
json.id article.id
json.title article.title
json.description article.description
json.cover_image cloud_cover_url(article.main_image)
json.published article.published
json.published_at article.published_at
json.tag_list article.cached_tag_list_array
json.slug article.slug
json.path article.path
json.url article.url
json.canonical_url article.processed_canonical_url
json.comments_count article.comments_count
json.positive_reactions_count article.positive_reactions_count
json.published_timestamp article.published_timestamp
json.body_markdown article.body_markdown
json.user do
json.name article.user.name
json.username article.user.username
json.twitter_username article.user.twitter_username
json.github_username article.user.github_username
json.website_url article.user.processed_website_url
json.profile_image ProfileImage.new(article.user).get(640)
json.profile_image_90 ProfileImage.new(article.user).get(90)
end
if article.organization
json.organization do
json.name article.organization.name
json.username article.organization.username
json.slug article.organization.slug
json.profile_image ProfileImage.new(article.organization).get(640)
json.profile_image_90 ProfileImage.new(article.organization).get(90)
end
end
if FlareTag.new(article).tag
json.flare_tag do
json.name FlareTag.new(article).tag.name
json.bg_color_hex FlareTag.new(article).tag.bg_color_hex
json.text_color_hex FlareTag.new(article).tag.text_color_hex
end
end
end

View file

@ -0,0 +1,11 @@
json.type_of "user"
json.id @user.id
json.username @user.username
json.name @user.name
json.summary @user.summary
json.twitter_username @user.twitter_username
json.github_username @user.github_username
json.website_url @user.website_url
json.location @user.location
json.joined_at @user.created_at.strftime("%b %e, %Y")
json.profile_image ProfileImage.new(@user).get(320)

View file

@ -149,7 +149,7 @@ Doorkeeper.configure do
# `grant_type` - the grant type of the request (see Doorkeeper::OAuth)
# `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes)
#
# use_refresh_token
use_refresh_token
# Provide support for an owner to be assigned to each registered application (disabled by default)
# Optional parameter confirmation: true (default false) if you want to enforce ownership of

View file

@ -78,6 +78,7 @@ Rails.application.routes.draw do
resources :articles, only: %i[index show create update] do
collection do
get "/onboarding", to: "articles#onboarding"
get :me
end
end
resources :comments, only: %i[index show]
@ -89,7 +90,11 @@ Rails.application.routes.draw do
post "/onboarding", to: "reactions#onboarding"
end
end
resources :users, only: %i[index show]
resources :users, only: %i[index show] do
collection do
get :me
end
end
resources :tags, only: [:index] do
collection do
get "/onboarding", to: "tags#onboarding"

View file

@ -0,0 +1,5 @@
FactoryBot.define do
factory :doorkeeper_access_token, class: "Doorkeeper::AccessToken" do
application
end
end

View file

@ -0,0 +1,8 @@
FactoryBot.define do
factory :application, class: "Doorkeeper::Application" do
sequence(:name) { |n| "Project #{n}" }
sequence(:redirect_uri) { |n| "https://example#{n}.com" }
secret { SecureRandom.hex(8) }
uid { SecureRandom.hex(8) }
end
end

View file

@ -120,3 +120,8 @@ RSpec.configure do |config|
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
end
Doorkeeper.configure do
# hash_token_secrets on its own won't work in test
hash_token_secrets fallback: :plain
end

View file

@ -102,6 +102,48 @@ RSpec.describe "Api::V0::Articles", type: :request do
end
end
describe "GET /api/articles/me" do
context "when request is unauthenticated" do
it "return unauthorized" do
get me_api_articles_path
expect(response).to have_http_status(:unauthorized)
end
end
context "when request is authenticated" do
let_it_be(:user) { create(:user) }
let_it_be(:access_token) { create :doorkeeper_access_token, resource_owner: user }
it "works with bearer authorization" do
headers = { "authorization" => "Bearer #{access_token.token}", "content-type" => "application/json" }
get me_api_articles_path, headers: headers
expect(response.content_type).to eq("application/json")
expect(response).to have_http_status(:ok)
end
it "return proper response specification" do
get me_api_articles_path, params: { access_token: access_token.token }
expect(response.content_type).to eq("application/json")
expect(response).to have_http_status(:ok)
end
it "return only user's articles including markdown" do
create(:article, user: user)
create(:article)
get me_api_articles_path, params: { access_token: access_token.token }
expect(json_response.length).to eq(1)
expect(json_response[0]["body_markdown"]).not_to be_nil
end
it "supports pagination" do
create_list(:article, 3, user: user)
get me_api_articles_path, params: { access_token: access_token.token, page: 2, per_page: 2 }
expect(json_response.length).to eq(1)
end
end
end
describe "POST /api/articles" do
let!(:api_secret) { create(:api_secret) }
let!(:user) { api_secret.user }
@ -131,6 +173,14 @@ RSpec.describe "Api::V0::Articles", type: :request do
post api_articles_path, params: { article: params }.to_json, headers: headers
end
it "supports oauth's access_token" do
access_token = create(:doorkeeper_access_token, resource_owner_id: user.id)
headers = { "authorization" => "Bearer #{access_token.token}", "content-type" => "application/json" }
post api_articles_path, params: { article: { title: Faker::Book.title } }.to_json, headers: headers
expect(response).to have_http_status(:created)
end
it "fails if no params are given" do
post_article
expect(response).to have_http_status(:unprocessable_entity)
@ -373,6 +423,19 @@ RSpec.describe "Api::V0::Articles", type: :request do
put path, params: { article: params }.to_json, headers: headers
end
it "supports oauth's access_token" do
access_token = create(:doorkeeper_access_token, resource_owner_id: user.id)
headers = { "authorization" => "Bearer #{access_token.token}", "content-type" => "application/json" }
title = Faker::Book.title + rand(100).to_s
body_markdown = "foobar"
params = { title: title, body_markdown: body_markdown }
put path, params: { article: params }.to_json, headers: headers
expect(response).to have_http_status(:ok)
expect(article.reload.title).to eq(title)
expect(article.body_markdown).to eq(body_markdown)
end
it "returns not found if the article does not belong to the user" do
article = create(:article, user: create(:user))
headers = { "api-key" => api_secret.secret, "content-type" => "application/json" }

View file

@ -0,0 +1,24 @@
require "rails_helper"
RSpec.describe "Api::V0::Users", type: :request do
def json_response
JSON.parse(response.body)
end
describe "GET /api/users/me" do
it "requires request to be authenticated" do
get me_api_users_path
expect(response).to have_http_status(:unauthorized)
end
context "when request is authenticated" do
let_it_be(:user) { create(:user) }
let_it_be(:access_token) { create(:doorkeeper_access_token, resource_owner: user) }
it "return user's information" do
get me_api_users_path, params: { access_token: access_token.token }
expect(json_response["username"]).to eq(user.username)
end
end
end
end