From 7fc7439ee7c3be9c6f7ce233a78a26bb2a4640b6 Mon Sep 17 00:00:00 2001 From: simukappu Date: Sun, 1 Dec 2019 18:47:36 +0900 Subject: [PATCH] Add API controllers integrated with Devise Token Auth - #108 #113 --- Gemfile | 1 + README.md | 156 +++++++++++++++++- activity_notification.gemspec | 2 + ...otifications_api_with_devise_controller.rb | 7 + ...ubscriptions_api_with_devise_controller.rb | 7 + spec/controllers/controller_spec_utility.rb | 30 +++- ...ications_api_controller_shared_examples.rb | 16 +- .../notifications_api_controller_spec.rb | 12 +- ...cations_api_with_devise_controller_spec.rb | 60 +++++++ ...tifications_with_devise_controller_spec.rb | 13 +- ...riptions_api_controller_shared_examples.rb | 32 ++-- .../subscriptions_api_controller_spec.rb | 12 +- ...iptions_api_with_devise_controller_spec.rb | 60 +++++++ ...bscriptions_with_devise_controller_spec.rb | 13 +- .../app/controllers/application_controller.rb | 2 +- spec/rails_app/app/models/user.rb | 19 ++- spec/rails_app/config/environment.rb | 3 +- .../config/initializers/devise_token_auth.rb | 55 ++++++ spec/rails_app/config/routes.rb | 17 +- .../20191201000000_add_tokens_to_users.rb | 10 ++ spec/rails_app/db/schema.rb | 5 +- 21 files changed, 458 insertions(+), 74 deletions(-) create mode 100644 app/controllers/activity_notification/notifications_api_with_devise_controller.rb create mode 100644 app/controllers/activity_notification/subscriptions_api_with_devise_controller.rb create mode 100644 spec/controllers/notifications_api_with_devise_controller_spec.rb create mode 100644 spec/controllers/subscriptions_api_with_devise_controller_spec.rb create mode 100644 spec/rails_app/config/initializers/devise_token_auth.rb create mode 100644 spec/rails_app/db/migrate/20191201000000_add_tokens_to_users.rb diff --git a/Gemfile b/Gemfile index c42d57cf..72c1cfad 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ group :production do gem 'puma' gem 'pg' gem 'devise' + gem 'devise_token_auth' end group :development do diff --git a/README.md b/README.md index 5a237493..aff67166 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ The deployed demo application is included in this gem's source code as a test ap - [Configuring integration with Devise authentication](#configuring-integration-with-devise-authentication) - [Using different model as target](#using-different-model-as-target) - [Configuring simple default routes](#configuring-simple-default-routes) + - [REST API backend with Devise Token Auth](#rest-api-backend-with-devise-token-auth) - [Push notification with Action Cable](#push-notification-with-action-cable) - [Enabling broadcasting notifications to channels](#enabling-broadcasting-notifications-to-channels) - [Subscribing notifications from channels](#subscribing-notifications-from-channels) @@ -1180,9 +1181,9 @@ If you would like to customize subscription controllers or views, you can use ge #### Configuring REST API backend -You can configure *activity_notification* routes as REST API backend with *api_mode* option of *notify_to* method. See [Routes as REST API backend](#routes-as-rest-api-backend) for more details. With *api_mode* option, *activity_notification* uses *[ActivityNotification::NotificationsApiController](/app/controllers/activity_notification/notifications_api_controller.rb)* instead of *[ActivityNotification::NotificationsController](/app/controllers/activity_notification/notifications_controller.rb)*. +You can configure *activity_notification* routes as REST API backend with **:api_mode** option of *notify_to* method. See [Routes as REST API backend](#routes-as-rest-api-backend) for more details. With *:api_mode* option, *activity_notification* uses *[ActivityNotification::NotificationsApiController](/app/controllers/activity_notification/notifications_api_controller.rb)* instead of *[ActivityNotification::NotificationsController](/app/controllers/activity_notification/notifications_controller.rb)*. -In addition, you can use *with_subscription* option with *api_mode* to enable subscription management like this: +In addition, you can use *:with_subscription* option with *:api_mode* to enable subscription management like this: ```ruby Rails.application.routes.draw do @@ -1196,6 +1197,8 @@ end Then, *activity_notification* uses *[ActivityNotification::SubscriptionsApiController](/app/controllers/activity_notification/subscriptions_api_controller.rb)* instead of *[ActivityNotification::SubscriptionsController](/app/controllers/activity_notification/subscriptions_controller.rb)*, and you can call *activity_notification* REST API as */api/v2/notifications* and */api/v2/subscriptions* from your frontend application. +When you want to use REST API backend integrated with Devise authentication, see [REST API backend with Devise Token Auth](#rest-api-backend-with-devise-token-auth). + #### API reference as OpenAPI Specification *activity_notification* provides API reference as [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification). @@ -1230,12 +1233,12 @@ Add **:with_devise** option in notification routing to *config/routes.rb* for th ```ruby Rails.application.routes.draw do devise_for :users - # Integrated with devise + # Integrated with Devise notify_to :users, with_devise: :users end ``` -Then *activity_notification* will use *[ActivityNotification::NotificationsWithDeviseController](/app/controllers/activity_notification/notifications_with_devise_controller.rb)* as a notification controller. The controller actions automatically call *authenticate_user!* and the user will be restricted to access and operate own notifications only, not others'. +Then *activity_notification* will use *[ActivityNotification::NotificationsWithDeviseController](/app/controllers/activity_notification/notifications_with_devise_controller.rb)* as a notifications controller. The controller actions automatically call *authenticate_user!* and the user will be restricted to access and operate own notifications only, not others'. *Hint*: HTTP 403 Forbidden will be returned for unauthorized notifications. @@ -1246,7 +1249,7 @@ You can also use different model from Devise resource as a target. When you will ```ruby Rails.application.routes.draw do devise_for :users - # Integrated with devise for different model + # Integrated with Devise for different model notify_to :admins, with_devise: :users end ``` @@ -1265,7 +1268,7 @@ In this example, *activity_notification* will confirm *admin* belonging to authe #### Configuring simple default routes -You can configure simple default routes for authenticated users, like */notifications* instead of */users/1/notifications*. Use *:devise_default_routes* option like this: +You can configure simple default routes for authenticated users, like */notifications* instead of */users/1/notifications*. Use **:devise_default_routes** option like this: ```ruby Rails.application.routes.draw do @@ -1279,7 +1282,7 @@ If you use multiple notification targets with Devise, you can also use this opti ```ruby Rails.application.routes.draw do devise_for :users - # Integrated with devise for different model, and use with scope + # Integrated with Devise for different model, and use with scope scope :admins, as: :admins do notify_to :admins, with_devise: :users, devise_default_routes: true, routing_scope: :admins end @@ -1288,6 +1291,145 @@ end Then, you can access */admins/notifications* instead of */admins/1/notifications*. +#### REST API backend with Devise Token Auth + +We can also integrate [REST API backend](#rest-api-backend) with [Devise Token Auth](https://github.com/lynndylanhurley/devise_token_auth). +Use **:with_devise** option with **:api_mode** option *config/routes.rb* for the target like this: + +```ruby +Rails.application.routes.draw do + devise_for :users + # Configure authentication API with Devise Token Auth + namespace :api do + scope :"v2" do + mount_devise_token_auth_for 'User', at: 'auth' + end + end + # Integrated with Devise Token Auth + scope :api do + scope :"v2" do + notify_to :users, api_mode: true, with_devise: :users, with_subscription: true + end + end +end +``` + +You can also configure it as simple default routes and with different model from Devise resource as a target: + +```ruby +Rails.application.routes.draw do + devise_for :users + # Configure authentication API with Devise Token Auth + namespace :api do + scope :"v2" do + mount_devise_token_auth_for 'User', at: 'auth' + end + end + # Integrated with Devise Token Auth as simple default routes and with different model from Devise resource as a target + scope :api do + scope :"v2" do + scope :admins, as: :admins do + notify_to :admins, api_mode: true, with_devise: :users, devise_default_routes: true, with_subscription: true + end + end + end +end +``` + +Then *activity_notification* will use *[ActivityNotification::NotificationsApiWithDeviseController](/app/controllers/activity_notification/notifications_api_with_devise_controller.rb)* as a notifications controller. The controller actions automatically call *authenticate_user!* and the user will be restricted to access and operate own notifications only, not others'. + +##### Configuring Devise Token Auth + +At first, you have to set up [Devise Token Auth configuration](https://devise-token-auth.gitbook.io/devise-token-auth/config). You also have to configure your target model like this: + +```ruby +class User < ActiveRecord::Base + devise :database_authenticatable, :confirmable + include DeviseTokenAuth::Concerns::User + acts_as_target +end +``` + +##### Using REST API backend with Devise Token Auth + +To sign in and get *access-token* from Devise Token Auth, call *sign_in* API which you configured by *mount_devise_token_auth_for* method: + +```console +$ curl -X POST -H "Content-Type: application/json" -D - -d '{"email": "ichiro@example.com","password": "changeit"}' https://activity-notification-example.herokuapp.com/api/v2/auth/sign_in + + +HTTP/1.1 200 OK +... +Content-Type: application/json; charset=utf-8 +access-token: ZiDvw8vJGtbESy5Qpw32Kw +token-type: Bearer +client: W0NkGrTS88xeOx4VDOS-Xg +expiry: 1576387310 +uid: ichiro@example.com +... + +{ + "data": { + "id": 1, + "email": "ichiro@example.com", + "provider": "email", + "uid": "ichiro@example.com", + "name": "Ichiro" + } +} +``` + +Then, call *activity_notification* API with returned *access-token*, *client* and *uid* as HTTP headers: + +```console +$ curl -X GET -H "Content-Type: application/json" -H "access-token: ZiDvw8vJGtbESy5Qpw32Kw" -H "client: W0NkGrTS88xeOx4VDOS-Xg" -H "uid: ichiro@example.com" -D - https://activity-notification-example.herokuapp.com/api/v2/notifications + +HTTP/1.1 200 OK +... + +{ + "count": 7, + "notifications": [ + ... + ] +} +``` + +Without valid *access-token*, API returns *401 Unauthorized*: + +```console +$ curl -X GET -H "Content-Type: application/json" -D - https://activity-notification-example.herokuapp.com/api/v2/notifications + +HTTP/1.1 401 Unauthorized +... + +{ + "errors": [ + "You need to sign in or sign up before continuing." + ] +} +``` + +When you request restricted resources of unauthorized targets, *activity_notification* API returns *403 Forbidden*: + +```console +$ curl -X GET -H "Content-Type: application/json" -H "access-token: ZiDvw8vJGtbESy5Qpw32Kw" -H "client: W0NkGrTS88xeOx4VDOS-Xg" -H "uid: ichiro@example.com" -D - https://activity-notification-example.herokuapp.com/api/v2/notifications/1 + +HTTP/1.1 403 Forbidden +... + +{ + "gem": "activity_notification", + "error": { + "code": 403, + "message": "Forbidden because of invalid parameter", + "type": "Wrong target is specified" + } +} +``` + +See [Devise Token Auth documents](https://devise-token-auth.gitbook.io/devise-token-auth/) for more details. + ### Push notification with Action Cable diff --git a/activity_notification.gemspec b/activity_notification.gemspec index 5435b452..ad1cb449 100644 --- a/activity_notification.gemspec +++ b/activity_notification.gemspec @@ -37,6 +37,8 @@ Gem::Specification.new do |s| s.add_development_dependency 'yard', '>= 0.9.16' s.add_development_dependency 'yard-activesupport-concern', '>= 0.0.1' s.add_development_dependency 'devise', '>= 4.5.0' + s.add_development_dependency 'devise_token_auth', '>= 1.1.3' + s.add_development_dependency 'mongoid-locker', '>= 2.0.0' s.add_development_dependency 'aws-sdk-sns', '~> 1' s.add_development_dependency 'slack-notifier', '>= 1.5.1' end diff --git a/app/controllers/activity_notification/notifications_api_with_devise_controller.rb b/app/controllers/activity_notification/notifications_api_with_devise_controller.rb new file mode 100644 index 00000000..d2b17ccc --- /dev/null +++ b/app/controllers/activity_notification/notifications_api_with_devise_controller.rb @@ -0,0 +1,7 @@ +module ActivityNotification + # Controller to manage notifications API with Devise authentication. + class NotificationsApiWithDeviseController < NotificationsApiController + include DeviseTokenAuth::Concerns::SetUserByToken + include DeviseAuthenticationController + end +end \ No newline at end of file diff --git a/app/controllers/activity_notification/subscriptions_api_with_devise_controller.rb b/app/controllers/activity_notification/subscriptions_api_with_devise_controller.rb new file mode 100644 index 00000000..b23b74ae --- /dev/null +++ b/app/controllers/activity_notification/subscriptions_api_with_devise_controller.rb @@ -0,0 +1,7 @@ +module ActivityNotification + # Controller to manage subscriptions API with Devise authentication. + class SubscriptionsApiWithDeviseController < SubscriptionsApiController + include DeviseTokenAuth::Concerns::SetUserByToken + include DeviseAuthenticationController + end +end \ No newline at end of file diff --git a/spec/controllers/controller_spec_utility.rb b/spec/controllers/controller_spec_utility.rb index 0a187f0e..d3732b42 100644 --- a/spec/controllers/controller_spec_utility.rb +++ b/spec/controllers/controller_spec_utility.rb @@ -93,11 +93,35 @@ def committee_options @committee_options ||= { schema: Committee::Drivers::load_from_file(schema_path), prefix: root_path, validate_success_only: true } end - def post_with_compatibility path, params + def get_with_compatibility path, options = {} if Rails::VERSION::MAJOR <= 4 - post path, params + get path, options[:params], options[:headers] else - post path, params: params + get path, options + end + end + + def post_with_compatibility path, options = {} + if Rails::VERSION::MAJOR <= 4 + post path, options[:params], options[:headers] + else + post path, options + end + end + + def put_with_compatibility path, options = {} + if Rails::VERSION::MAJOR <= 4 + put path, options[:params], options[:headers] + else + put path, options + end + end + + def delete_with_compatibility path, options = {} + if Rails::VERSION::MAJOR <= 4 + delete path, options[:params], options[:headers] + else + delete path, options end end diff --git a/spec/controllers/notifications_api_controller_shared_examples.rb b/spec/controllers/notifications_api_controller_shared_examples.rb index 3ebb38a8..9645ad2d 100644 --- a/spec/controllers/notifications_api_controller_shared_examples.rb +++ b/spec/controllers/notifications_api_controller_shared_examples.rb @@ -454,52 +454,52 @@ describe "GET /{target_type}/{target_id}/notifications", type: :request do it "returns response as API references" do - get "#{api_path}/notifications" + get_with_compatibility "#{api_path}/notifications", headers: @headers assert_all_schema_confirm(response, 200) end end describe "POST /{target_type}/{target_id}/notifications/open_all", type: :request do it "returns response as API references" do - post "#{api_path}/notifications/open_all" + post_with_compatibility "#{api_path}/notifications/open_all", headers: @headers assert_all_schema_confirm(response, 200) end end describe "GET /{target_type}/{target_id}/notifications/{id}", type: :request do it "returns response as API references" do - get "#{api_path}/notifications/#{@notification.id}" + get_with_compatibility "#{api_path}/notifications/#{@notification.id}", headers: @headers assert_all_schema_confirm(response, 200) end it "returns error response as API references" do - get "#{api_path}/notifications/0" + get_with_compatibility "#{api_path}/notifications/0", headers: @headers assert_all_schema_confirm(response, 404) end end describe "DELETE /{target_type}/{target_id}/notifications/{id}", type: :request do it "returns response as API references" do - delete "#{api_path}/notifications/#{@notification.id}" + delete_with_compatibility "#{api_path}/notifications/#{@notification.id}", headers: @headers assert_all_schema_confirm(response, 204) end end describe "PUT /{target_type}/{target_id}/notifications/{id}/open", type: :request do it "returns response as API references" do - put "#{api_path}/notifications/#{@notification.id}/open" + put_with_compatibility "#{api_path}/notifications/#{@notification.id}/open", headers: @headers assert_all_schema_confirm(response, 200) end it "returns response as API references when request parameters have move=true" do - put "#{api_path}/notifications/#{@notification.id}/open?move=true" + put_with_compatibility "#{api_path}/notifications/#{@notification.id}/open?move=true", headers: @headers assert_all_schema_confirm(response, 302) end end describe "GET /{target_type}/{target_id}/notifications/{id}/move", type: :request do it "returns response as API references" do - get "#{api_path}/notifications/#{@notification.id}/move" + get_with_compatibility "#{api_path}/notifications/#{@notification.id}/move", headers: @headers assert_all_schema_confirm(response, 302) end end diff --git a/spec/controllers/notifications_api_controller_spec.rb b/spec/controllers/notifications_api_controller_spec.rb index 85fb4d15..d522fad1 100644 --- a/spec/controllers/notifications_api_controller_spec.rb +++ b/spec/controllers/notifications_api_controller_spec.rb @@ -8,12 +8,12 @@ let(:valid_session) {} it_behaves_like :notifications_api_controller -end -RSpec.describe "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}", type: :request do - let(:root_path) { "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}" } - let(:test_target) { create(:user) } - let(:target_type) { :users } + describe "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}", type: :request do + let(:root_path) { "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}" } + let(:test_target) { create(:user) } + let(:target_type) { :users } - it_behaves_like :notifications_api_request + it_behaves_like :notifications_api_request + end end \ No newline at end of file diff --git a/spec/controllers/notifications_api_with_devise_controller_spec.rb b/spec/controllers/notifications_api_with_devise_controller_spec.rb new file mode 100644 index 00000000..2fc9b28e --- /dev/null +++ b/spec/controllers/notifications_api_with_devise_controller_spec.rb @@ -0,0 +1,60 @@ +require 'controllers/notifications_api_controller_shared_examples' + +context "ActivityNotification::NotificationsApiWithDeviseController" do + context "test admins API with associated users authentication" do + + describe "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}", type: :request do + include ActivityNotification::ControllerSpec::CommitteeUtility + + let(:root_path) { "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}" } + let(:test_user) { create(:confirmed_user) } + let(:unauthenticated_user) { create(:confirmed_user) } + let(:test_target) { create(:admin, user: test_user) } + let(:target_type) { :admins } + + def sign_in_with_devise_token_auth(auth_user, status) + post_with_compatibility "#{root_path}/auth/sign_in", params: { email: auth_user.email, password: "password" } + expect(response).to have_http_status(status) + @headers = response.header.slice("access-token", "client", "uid") + end + + context "signed in with devise as authenticated user" do + before do + sign_in_with_devise_token_auth(test_user, 200) + end + + it_behaves_like :notifications_api_request + end + + context "signed in with devise as unauthenticated user" do + let(:target_params) { { target_type: target_type, devise_type: :users } } + + describe "GET #index" do + before do + sign_in_with_devise_token_auth(unauthenticated_user, 200) + get_with_compatibility "#{api_path}/notifications", headers: @headers + end + + it "returns 403 as http status code" do + expect(response.status).to eq(403) + end + end + end + + context "unsigned in with devise" do + let(:target_params) { { target_type: target_type, devise_type: :users } } + + describe "GET #index" do + before do + get_with_compatibility "#{api_path}/notifications", headers: @headers + end + + it "returns 401 as http status code" do + expect(response.status).to eq(401) + end + end + end + end + + end +end \ No newline at end of file diff --git a/spec/controllers/notifications_with_devise_controller_spec.rb b/spec/controllers/notifications_with_devise_controller_spec.rb index c342c572..7d0a3b7d 100644 --- a/spec/controllers/notifications_with_devise_controller_spec.rb +++ b/spec/controllers/notifications_with_devise_controller_spec.rb @@ -1,6 +1,8 @@ require 'controllers/notifications_controller_shared_examples' describe ActivityNotification::NotificationsWithDeviseController, type: :controller do + include ActivityNotification::ControllerSpec::RequestUtility + let(:test_user) { create(:confirmed_user) } let(:unauthenticated_user) { create(:confirmed_user) } let(:test_target) { create(:admin, user: test_user) } @@ -92,15 +94,4 @@ end end end - - private - - def get_with_compatibility action, params, session - if Rails::VERSION::MAJOR <= 4 - get action, params, session - else - get action, params: params, session: session - end - end - end diff --git a/spec/controllers/subscriptions_api_controller_shared_examples.rb b/spec/controllers/subscriptions_api_controller_shared_examples.rb index bdec188a..69aa5b8f 100644 --- a/spec/controllers/subscriptions_api_controller_shared_examples.rb +++ b/spec/controllers/subscriptions_api_controller_shared_examples.rb @@ -593,7 +593,7 @@ clean_database end - describe "GET /apidocs" do + describe "GET /apidocs to test" do it "returns API references as OpenAPI Specification JSON schema" do get "#{root_path}/apidocs" write_schema_file(response.body) @@ -603,97 +603,97 @@ describe "GET /{target_type}/{target_id}/subscriptions", type: :request do it "returns response as API references" do - get "#{api_path}/subscriptions" + get_with_compatibility "#{api_path}/subscriptions", headers: @headers assert_all_schema_confirm(response, 200) end end describe "POST /{target_type}/{target_id}/subscriptions", type: :request do it "returns response as API references" do - post_with_compatibility "#{api_path}/subscriptions", { + post_with_compatibility "#{api_path}/subscriptions", params: { "subscription" => { "key" => "new_subscription_key", "subscribing"=> "true", "subscribing_to_email"=>"true" } - } + }, headers: @headers assert_all_schema_confirm(response, 201) end it "returns response as API references when the key is duplicate" do - post_with_compatibility "#{api_path}/subscriptions", { + post_with_compatibility "#{api_path}/subscriptions", params: { "subscription" => { "key" => "configured_key", "subscribing"=> "true", "subscribing_to_email"=>"true" } - } + }, headers: @headers assert_all_schema_confirm(response, 409) end end describe "GET /{target_type}/{target_id}/subscriptions/find", type: :request do it "returns response as API references" do - get "#{api_path}/subscriptions/find?key=#{subscription.key}" + get_with_compatibility "#{api_path}/subscriptions/find?key=#{@subscription.key}", headers: @headers assert_all_schema_confirm(response, 200) end end describe "GET /{target_type}/{target_id}/subscriptions/{id}", type: :request do it "returns response as API references" do - get "#{api_path}/subscriptions/#{subscription.id}" + get_with_compatibility "#{api_path}/subscriptions/#{@subscription.id}", headers: @headers assert_all_schema_confirm(response, 200) end it "returns error response as API references" do - get "#{api_path}/subscriptions/0" + get_with_compatibility "#{api_path}/subscriptions/0", headers: @headers assert_all_schema_confirm(response, 404) end end describe "DELETE /{target_type}/{target_id}/subscriptions/{id}", type: :request do it "returns response as API references" do - delete "#{api_path}/subscriptions/#{subscription.id}" + delete_with_compatibility "#{api_path}/subscriptions/#{@subscription.id}", headers: @headers assert_all_schema_confirm(response, 204) end end describe "PUT /{target_type}/{target_id}/subscriptions/{id}/subscribe", type: :request do it "returns response as API references" do - put "#{api_path}/subscriptions/#{subscription.id}/subscribe" + put_with_compatibility "#{api_path}/subscriptions/#{@subscription.id}/subscribe", headers: @headers assert_all_schema_confirm(response, 200) end end describe "PUT /{target_type}/{target_id}/subscriptions/{id}/unsubscribe", type: :request do it "returns response as API references" do - put "#{api_path}/subscriptions/#{subscription.id}/unsubscribe" + put_with_compatibility "#{api_path}/subscriptions/#{@subscription.id}/unsubscribe", headers: @headers assert_all_schema_confirm(response, 200) end end describe "PUT /{target_type}/{target_id}/subscriptions/{id}/subscribe_to_email", type: :request do it "returns response as API references" do - put "#{api_path}/subscriptions/#{subscription.id}/subscribe_to_email" + put_with_compatibility "#{api_path}/subscriptions/#{@subscription.id}/subscribe_to_email", headers: @headers assert_all_schema_confirm(response, 200) end end describe "PUT /{target_type}/{target_id}/subscriptions/{id}/unsubscribe_to_email", type: :request do it "returns response as API references" do - put "#{api_path}/subscriptions/#{subscription.id}/unsubscribe_to_email" + put_with_compatibility "#{api_path}/subscriptions/#{@subscription.id}/unsubscribe_to_email", headers: @headers assert_all_schema_confirm(response, 200) end end describe "PUT /{target_type}/{target_id}/subscriptions/{id}/subscribe_to_optional_target", type: :request do it "returns response as API references" do - put "#{api_path}/subscriptions/#{subscription.id}/subscribe_to_optional_target?optional_target_name=slack" + put_with_compatibility "#{api_path}/subscriptions/#{@subscription.id}/subscribe_to_optional_target?optional_target_name=slack", headers: @headers assert_all_schema_confirm(response, 200) end end describe "PUT /{target_type}/{target_id}/subscriptions/{id}/unsubscribe_to_optional_target", type: :request do it "returns response as API references" do - put "#{api_path}/subscriptions/#{subscription.id}/unsubscribe_to_optional_target?optional_target_name=slack" + put_with_compatibility "#{api_path}/subscriptions/#{@subscription.id}/unsubscribe_to_optional_target?optional_target_name=slack", headers: @headers assert_all_schema_confirm(response, 200) end end diff --git a/spec/controllers/subscriptions_api_controller_spec.rb b/spec/controllers/subscriptions_api_controller_spec.rb index 4d841beb..aa013b80 100644 --- a/spec/controllers/subscriptions_api_controller_spec.rb +++ b/spec/controllers/subscriptions_api_controller_spec.rb @@ -8,12 +8,12 @@ let(:valid_session) {} it_behaves_like :subscriptions_api_controller -end -RSpec.describe "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}", type: :request do - let(:root_path) { "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}" } - let(:test_target) { create(:user) } - let(:target_type) { :users } + describe "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}", type: :request do + let(:root_path) { "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}" } + let(:test_target) { create(:user) } + let(:target_type) { :users } - it_behaves_like :subscriptions_api_request + it_behaves_like :subscriptions_api_request + end end \ No newline at end of file diff --git a/spec/controllers/subscriptions_api_with_devise_controller_spec.rb b/spec/controllers/subscriptions_api_with_devise_controller_spec.rb new file mode 100644 index 00000000..e3843a7e --- /dev/null +++ b/spec/controllers/subscriptions_api_with_devise_controller_spec.rb @@ -0,0 +1,60 @@ +require 'controllers/subscriptions_api_controller_shared_examples' + +context "ActivityNotification::NotificationsApiWithDeviseController" do + context "test admins API with associated users authentication" do + + describe "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}", type: :request do + include ActivityNotification::ControllerSpec::CommitteeUtility + + let(:root_path) { "/api/v#{ActivityNotification::GEM_VERSION::MAJOR}" } + let(:test_user) { create(:confirmed_user) } + let(:unauthenticated_user) { create(:confirmed_user) } + let(:test_target) { create(:admin, user: test_user) } + let(:target_type) { :admins } + + def sign_in_with_devise_token_auth(auth_user, status) + post_with_compatibility "#{root_path}/auth/sign_in", params: { email: auth_user.email, password: "password" } + expect(response).to have_http_status(status) + @headers = response.header.slice("access-token", "client", "uid") + end + + context "signed in with devise as authenticated user" do + before do + sign_in_with_devise_token_auth(test_user, 200) + end + + it_behaves_like :subscriptions_api_request + end + + context "signed in with devise as unauthenticated user" do + let(:target_params) { { target_type: target_type, devise_type: :users } } + + describe "GET #index" do + before do + sign_in_with_devise_token_auth(unauthenticated_user, 200) + get_with_compatibility "#{api_path}/subscriptions", headers: @headers + end + + it "returns 403 as http status code" do + expect(response.status).to eq(403) + end + end + end + + context "unsigned in with devise" do + let(:target_params) { { target_type: target_type, devise_type: :users } } + + describe "GET #index" do + before do + get_with_compatibility "#{api_path}/subscriptions", headers: @headers + end + + it "returns 401 as http status code" do + expect(response.status).to eq(401) + end + end + end + end + + end +end \ No newline at end of file diff --git a/spec/controllers/subscriptions_with_devise_controller_spec.rb b/spec/controllers/subscriptions_with_devise_controller_spec.rb index c9d0f36e..4fd7f0bc 100644 --- a/spec/controllers/subscriptions_with_devise_controller_spec.rb +++ b/spec/controllers/subscriptions_with_devise_controller_spec.rb @@ -1,6 +1,8 @@ require 'controllers/subscriptions_controller_shared_examples' describe ActivityNotification::SubscriptionsWithDeviseController, type: :controller do + include ActivityNotification::ControllerSpec::RequestUtility + let(:test_user) { create(:confirmed_user) } let(:unauthenticated_user) { create(:confirmed_user) } let(:test_target) { create(:admin, user: test_user) } @@ -92,15 +94,4 @@ end end end - - private - - def get_with_compatibility action, params, session - if Rails::VERSION::MAJOR <= 4 - get action, params, session - else - get action, params: params, session: session - end - end - end diff --git a/spec/rails_app/app/controllers/application_controller.rb b/spec/rails_app/app/controllers/application_controller.rb index d83690e1..9e7c494c 100644 --- a/spec/rails_app/app/controllers/application_controller.rb +++ b/spec/rails_app/app/controllers/application_controller.rb @@ -1,5 +1,5 @@ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. - protect_from_forgery with: :exception + protect_from_forgery with: :null_session end diff --git a/spec/rails_app/app/models/user.rb b/spec/rails_app/app/models/user.rb index d83d7c22..6757f269 100644 --- a/spec/rails_app/app/models/user.rb +++ b/spec/rails_app/app/models/user.rb @@ -1,6 +1,7 @@ unless ENV['AN_TEST_DB'] == 'mongodb' class User < ActiveRecord::Base devise :database_authenticatable, :confirmable + include DeviseTokenAuth::Concerns::User validates :email, presence: true has_many :articles, dependent: :destroy has_one :admin, dependent: :destroy @@ -16,12 +17,15 @@ def admin? end else require 'mongoid' + require 'mongoid-locker' class User include Mongoid::Document include Mongoid::Timestamps + include Mongoid::Locker include GlobalID::Identification devise :database_authenticatable, :confirmable + include DeviseTokenAuth::Concerns::User has_many :articles, dependent: :destroy has_one :admin, dependent: :destroy validates :email, presence: true @@ -33,6 +37,11 @@ class User field :confirmation_token, type: String field :confirmed_at, type: Time field :confirmation_sent_at, type: Time + ## Required + field :provider, type: String, default: "email" + field :uid, type: String, default: "" + ## Tokens + field :tokens, type: Hash, default: {} # Apps field :name, type: String @@ -40,10 +49,18 @@ class User acts_as_target email: :email, email_allowed: :confirmed_at, batch_email_allowed: :confirmed_at, subscription_allowed: true, printable_name: :name, action_cable_allowed: true, action_cable_with_devise: true -acts_as_notifier printable_name: :name + acts_as_notifier printable_name: :name def admin? admin.present? end + + # To avoid Devise Token Auth issue + # https://github.com/lynndylanhurley/devise_token_auth/issues/1335 + if Rails::VERSION::MAJOR == 6 + def saved_change_to_attribute?(attr_name, **options) + true + end + end end end diff --git a/spec/rails_app/config/environment.rb b/spec/rails_app/config/environment.rb index 8bccec74..2775e1dd 100644 --- a/spec/rails_app/config/environment.rb +++ b/spec/rails_app/config/environment.rb @@ -1,8 +1,9 @@ # Load the Rails application. require File.expand_path('../application', __FILE__) -# Demo application uses Devise +# Demo application uses Devise and Devise Token Auth require 'devise' +require 'devise_token_auth' # Initialize the Rails application. Rails.application.initialize! diff --git a/spec/rails_app/config/initializers/devise_token_auth.rb b/spec/rails_app/config/initializers/devise_token_auth.rb new file mode 100644 index 00000000..72b13c1b --- /dev/null +++ b/spec/rails_app/config/initializers/devise_token_auth.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +DeviseTokenAuth.setup do |config| + # By default the authorization headers will change after each request. The + # client is responsible for keeping track of the changing tokens. Change + # this to false to prevent the Authorization header from changing after + # each request. + # config.change_headers_on_each_request = true + + # By default, users will need to re-authenticate after 2 weeks. This setting + # determines how long tokens will remain valid after they are issued. + # config.token_lifespan = 2.weeks + + # Limiting the token_cost to just 4 in testing will increase the performance of + # your test suite dramatically. The possible cost value is within range from 4 + # to 31. It is recommended to not use a value more than 10 in other environments. + config.token_cost = Rails.env.test? ? 4 : 10 + + # Sets the max number of concurrent devices per user, which is 10 by default. + # After this limit is reached, the oldest tokens will be removed. + # config.max_number_of_devices = 10 + + # Sometimes it's necessary to make several requests to the API at the same + # time. In this case, each request in the batch will need to share the same + # auth token. This setting determines how far apart the requests can be while + # still using the same auth token. + # config.batch_request_buffer_throttle = 5.seconds + + # This route will be the prefix for all oauth2 redirect callbacks. For + # example, using the default '/omniauth', the github oauth2 provider will + # redirect successful authentications to '/omniauth/github/callback' + # config.omniauth_prefix = "/omniauth" + + # By default sending current password is not needed for the password update. + # Uncomment to enforce current_password param to be checked before all + # attribute updates. Set it to :password if you want it to be checked only if + # password is updated. + # config.check_current_password_before_update = :attributes + + # By default we will use callbacks for single omniauth. + # It depends on fields like email, provider and uid. + # config.default_callbacks = true + + # Makes it possible to change the headers names + # config.headers_names = {:'access-token' => 'access-token', + # :'client' => 'client', + # :'expiry' => 'expiry', + # :'uid' => 'uid', + # :'token-type' => 'token-type' } + + # By default, only Bearer Token authentication is implemented out of the box. + # If, however, you wish to integrate with legacy Devise authentication, you can + # do so by enabling this flag. NOTE: This feature is highly experimental! + # config.enable_standard_devise_support = false +end \ No newline at end of file diff --git a/spec/rails_app/config/routes.rb b/spec/rails_app/config/routes.rb index 79661390..6fa3ec19 100644 --- a/spec/rails_app/config/routes.rb +++ b/spec/rails_app/config/routes.rb @@ -7,11 +7,15 @@ notify_to :users, with_subscription: true notify_to :users, with_devise: :users, devise_default_routes: true, with_subscription: true + namespace :api do + scope :"v#{ActivityNotification::GEM_VERSION::MAJOR}" do + mount_devise_token_auth_for 'User', at: 'auth' + end + end scope :api do scope :"v#{ActivityNotification::GEM_VERSION::MAJOR}" do notify_to :users, api_mode: true, with_subscription: true - #TODO - # notify_to :users, api_mode: true, with_devise: :users, devise_default_routes: true, with_subscription: true + notify_to :users, api_mode: true, with_devise: :users, devise_default_routes: true, with_subscription: true resources :apidocs, only: [:index], controller: 'activity_notification/apidocs' end end @@ -20,4 +24,13 @@ scope :admins, as: :admins do notify_to :admins, with_devise: :users, devise_default_routes: true, with_subscription: true, routing_scope: :admins end + + scope :api do + scope :"v#{ActivityNotification::GEM_VERSION::MAJOR}" do + notify_to :admins, api_mode: true, with_devise: :users, with_subscription: true + scope :admins, as: :admins do + notify_to :admins, api_mode: true, with_devise: :users, devise_default_routes: true, with_subscription: true + end + end + end end diff --git a/spec/rails_app/db/migrate/20191201000000_add_tokens_to_users.rb b/spec/rails_app/db/migrate/20191201000000_add_tokens_to_users.rb new file mode 100644 index 00000000..9bbe3708 --- /dev/null +++ b/spec/rails_app/db/migrate/20191201000000_add_tokens_to_users.rb @@ -0,0 +1,10 @@ +class AddTokensToUsers < ActiveRecord::Migration[5.2] + def change + ## Required + add_column :users, :provider, :string, null: false, default: "email" + add_column :users, :uid, :string, null: false, default: "" + + ## Tokens + add_column :users, :tokens, :text + end +end \ No newline at end of file diff --git a/spec/rails_app/db/schema.rb b/spec/rails_app/db/schema.rb index 63b05dce..dfb4dca0 100644 --- a/spec/rails_app/db/schema.rb +++ b/spec/rails_app/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_12_09_000000) do +ActiveRecord::Schema.define(version: 2019_12_01_000000) do create_table "admins", force: :cascade do |t| t.integer "user_id" @@ -89,6 +89,9 @@ t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "provider", default: "email", null: false + t.string "uid", default: "", null: false + t.text "tokens" t.index ["email"], name: "index_users_on_email" end