Skip to content

Commit

Permalink
Add API controllers integrated with Devise Token Auth - #108 #113
Browse files Browse the repository at this point in the history
  • Loading branch information
simukappu committed Dec 1, 2019
1 parent 7a98f87 commit 95aeef0
Show file tree
Hide file tree
Showing 21 changed files with 458 additions and 74 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ group :production do
gem 'puma'
gem 'pg'
gem 'devise'
gem 'devise_token_auth'
end

group :development do
Expand Down
156 changes: 149 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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).
Expand Down Expand Up @@ -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.

Expand All @@ -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
```
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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": "[email protected]","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: [email protected]
...

{
"data": {
"id": 1,
"email": "[email protected]",
"provider": "email",
"uid": "[email protected]",
"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: [email protected]" -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: [email protected]" -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

Expand Down
2 changes: 2 additions & 0 deletions activity_notification.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
30 changes: 27 additions & 3 deletions spec/controllers/controller_spec_utility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 8 additions & 8 deletions spec/controllers/notifications_api_controller_shared_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions spec/controllers/notifications_api_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 95aeef0

Please sign in to comment.