devise-api 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ module Api
5
+ module ResourceOwnerService
6
+ class SignUp < Devise::Api::BaseService
7
+ option :params, type: Types::Hash
8
+ option :resource_class, type: Types::Class
9
+
10
+ def call
11
+ ActiveRecord::Base.transaction do
12
+ resource_owner = yield create_resource_owner
13
+ devise_api_token = yield call_create_devise_api_token_service(resource_owner)
14
+
15
+ Success(devise_api_token)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def create_resource_owner
22
+ resource_owner = resource_class.new(params)
23
+
24
+ return Success(resource_owner) if resource_owner.save
25
+
26
+ Failure(error: :resource_owner_create_error, record: resource_owner)
27
+ end
28
+
29
+ def call_create_devise_api_token_service(resource_owner)
30
+ Devise::Api::TokensService::Create.new(resource_owner: resource_owner).call
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ module Api
5
+ module TokensService
6
+ class Create < Devise::Api::BaseService
7
+ option :resource_owner
8
+ option :previous_refresh_token, type: Types::String | Types::Nil, default: proc { nil }
9
+
10
+ def call
11
+ return Failure(:invalid_resource_owner) unless resource_owner.respond_to?(:access_tokens)
12
+
13
+ devise_api_token = yield create_devise_api_token
14
+
15
+ Success(devise_api_token)
16
+ end
17
+
18
+ private
19
+
20
+ def authenticate_service
21
+ Devise::Api::ResourceOwnerService::Authenticate.new(params: params,
22
+ resource_class: resource_class).call
23
+ end
24
+
25
+ def create_devise_api_token
26
+ devise_api_token = resource_owner.access_tokens.new(params)
27
+
28
+ return Success(devise_api_token) if devise_api_token.save
29
+
30
+ Failure(error: :devise_api_token_create_error, record: devise_api_token)
31
+ end
32
+
33
+ def params
34
+ {
35
+ access_token: Devise.api.config.base_token_model.constantize.generate_uniq_access_token(resource_owner),
36
+ refresh_token: Devise.api.config.base_token_model.constantize.generate_uniq_refresh_token(resource_owner),
37
+ expires_in: Devise.api.config.access_token.expires_in,
38
+ revoked_at: nil,
39
+ previous_refresh_token: previous_refresh_token
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ module Api
5
+ module TokensService
6
+ class Refresh < Devise::Api::BaseService
7
+ option :devise_api_token, type: Types.Instance(Devise.api.base_token_model.constantize)
8
+ option :resource_owner, default: proc { devise_api_token.resource_owner }
9
+
10
+ def call
11
+ return Failure(:expired_refresh_token) if devise_api_token.refresh_token_expired?
12
+
13
+ devise_api_token = yield create_devise_api_token
14
+ Success(devise_api_token)
15
+ end
16
+
17
+ private
18
+
19
+ def create_devise_api_token
20
+ Devise::Api::TokensService::Create.new(resource_owner: resource_owner,
21
+ previous_refresh_token: devise_api_token.refresh_token).call
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ module Api
5
+ module TokensService
6
+ class Revoke < Devise::Api::BaseService
7
+ option :devise_api_token, optional: true
8
+
9
+ def call
10
+ return Success(devise_api_token) if devise_api_token.blank?
11
+ return Success(devise_api_token) if devise_api_token.revoked? || devise_api_token.expired?
12
+ return Success(devise_api_token) if devise_api_token.update(revoked_at: Time.zone.now)
13
+
14
+ Failure(error: :devise_api_token_revoke_error, record: devise_api_token)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ en:
2
+ devise:
3
+ api:
4
+ error_response:
5
+ invalid_authentication: "Email or password is invalid"
6
+ invalid_token: "Invalid token"
7
+ expired_token: "Token has expired"
8
+ expired_refresh_token: "Refresh token has expired"
9
+ revoked_token: "Token has been revoked"
10
+ refresh_token_disabled: "Refresh token is disabled for this application"
11
+ invalid_refresh_token: "Refresh token is invalid"
12
+ invalid_email: "Email is invalid"
13
+ invalid_resource_owner: "Resource owner is invalid"
14
+ resource_owner_create_error: "Resource owner could not be created"
15
+ devise_api_token_create_error: "Token could not be created"
16
+ devise_api_token_revoke_error: "Token could not be revoked"
17
+ lockable:
18
+ locked: "Your account is locked"
19
+ confirmable:
20
+ unconfirmed: "You have to confirm your account before continuing"
21
+ registerable:
22
+ signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account"
data/devise-api.gemspec CHANGED
@@ -2,15 +2,18 @@
2
2
 
3
3
  require_relative 'lib/devise/api/version'
4
4
 
5
+ # rubocop:disable Layout/LineLength
5
6
  Gem::Specification.new do |spec|
6
7
  spec.name = 'devise-api'
7
8
  spec.version = Devise::Api::VERSION
8
9
  spec.authors = ['nejdetkadir']
9
10
  spec.email = ['nejdetkadir.550@gmail.com']
10
11
 
11
- spec.summary = "It provides support for access tokens and refresh tokens,
12
- which allow you to authenticate API requests and keep the user's
13
- session active for a longer period of time."
12
+ spec.summary = "The devise-api gem is a convenient way to add authentication to your Ruby on Rails application using the devise gem.
13
+ It provides support for access tokens and refresh tokens, which allow you to authenticate API requests and
14
+ keep the user's session active for a longer period of time on the client side. It can be installed by adding the gem to your Gemfile,
15
+ running migrations, and adding the :api module to your devise model. The gem is fully configurable,
16
+ allowing you to set things like token expiration times and token generators."
14
17
 
15
18
  spec.description = spec.summary
16
19
  spec.homepage = "https://github.com/nejdetkadir/#{spec.name}"
@@ -33,9 +36,14 @@ Gem::Specification.new do |spec|
33
36
  spec.require_paths = ['lib']
34
37
 
35
38
  # Uncomment to register a new dependency of your gem
36
- spec.add_dependency 'rails', '>= 6.0.0'
37
39
  spec.add_dependency 'devise', '>= 4.7.2'
40
+ spec.add_dependency 'dry-configurable', '~> 1.0', '>= 1.0.1'
41
+ spec.add_dependency 'dry-initializer', '>= 3.1.1'
42
+ spec.add_dependency 'dry-monads', '>= 1.6.0'
43
+ spec.add_dependency 'dry-types', '>= 1.7.0'
44
+ spec.add_dependency 'rails', '>= 6.0.0'
38
45
 
39
46
  # For more information and examples about making a new gem, check out our
40
47
  # guide at: https://bundler.io/guides/creating_gem.html
41
48
  end
49
+ # rubocop:enable Layout/LineLength
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-configurable'
4
+
5
+ module Devise
6
+ module Api
7
+ class Configuration
8
+ include Dry::Configurable
9
+
10
+ setting :access_token, reader: true do
11
+ setting :expires_in, default: 1.hour, reader: true
12
+ setting :expires_in_infinite, default: proc { |_resource_owner| false }, reader: true
13
+ setting :generator, default: proc { |_resource_owner| ::Devise.friendly_token(60) }, reader: true
14
+ end
15
+
16
+ setting :refresh_token, reader: true do
17
+ setting :enabled, default: true, reader: true
18
+ setting :expires_in, default: 1.week, reader: true
19
+ setting :generator, default: proc { |_resource_owner| ::Devise.friendly_token(60) }, reader: true
20
+ setting :expires_in_infinite, default: proc { |_resource_owner| false }, reader: true
21
+ end
22
+
23
+ setting :authorization, reader: true do
24
+ setting :key, default: 'Authorization', reader: true
25
+ setting :scheme, default: 'Bearer', reader: true
26
+ setting :location, default: :both, reader: true # :header or :params or :both
27
+ setting :params_key, default: 'access_token', reader: true
28
+ end
29
+
30
+ setting :base_token_model, default: 'Devise::Api::Token', reader: true
31
+ setting :base_controller, default: '::DeviseController', reader: true
32
+
33
+ setting :after_successful_sign_in, default: proc { |_resource_owner, _token, _request| }, reader: true
34
+ setting :after_successful_sign_up, default: proc { |_resource_owner, _token, _request| }, reader: true
35
+ setting :after_successful_refresh, default: proc { |_resource_owner, _token, _request| }, reader: true
36
+ setting :after_successful_revoke, default: proc { |_resource_owner, _token, _request| }, reader: true
37
+
38
+ setting :before_sign_in, default: proc { |_params, _request, _resource_class| }, reader: true
39
+ setting :before_sign_up, default: proc { |_params, _request, _resource_class| }, reader: true
40
+ setting :before_refresh, default: proc { |_token, _request| }, reader: true
41
+ setting :before_revoke, default: proc { |_token, _request| }, reader: true
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Devise
6
+ module Api
7
+ module Controllers
8
+ module Helpers
9
+ extend ActiveSupport::Concern
10
+
11
+ def authenticate_devise_api_token!
12
+ if current_devise_api_token.blank?
13
+ error_response = Devise::Api::Responses::ErrorResponse.new(request, error: :invalid_token,
14
+ resource_class: resource_class)
15
+
16
+ return render json: error_response.body, status: error_response.status
17
+ end
18
+
19
+ if current_devise_api_token.expired?
20
+ error_response = Devise::Api::Responses::ErrorResponse.new(request, error: :expired_token,
21
+ resource_class: resource_class)
22
+
23
+ return render json: error_response.body, status: error_response.status
24
+ end
25
+
26
+ return unless current_devise_api_token.revoked?
27
+
28
+ error_response = Devise::Api::Responses::ErrorResponse.new(request, error: :revoked_token,
29
+ resource_class: resource_class)
30
+
31
+ render json: error_response.body, status: error_response.status
32
+ end
33
+
34
+ def current_devise_api_refresh_token
35
+ token = find_devise_api_token
36
+
37
+ Devise.api.config.base_token_model.constantize.find_by(refresh_token: token)
38
+ end
39
+
40
+ def current_devise_api_token
41
+ token = find_devise_api_token
42
+
43
+ devise_api_token_model = Devise.api.config.base_token_model.constantize
44
+
45
+ if Devise.api.config.refresh_token.enabled
46
+ return devise_api_token_model
47
+ .where(access_token: token)
48
+ .or(devise_api_token_model.where(refresh_token: token))
49
+ &.first
50
+ end
51
+
52
+ devise_api_token_model.find_by(access_token: token)
53
+ end
54
+
55
+ def current_devise_api_user
56
+ current_devise_api_token&.resource_owner
57
+ end
58
+
59
+ private
60
+
61
+ def extract_devise_api_token_from_params
62
+ params[Devise.api.config.authorization.params_key]
63
+ end
64
+
65
+ def extract_devise_api_token_from_headers
66
+ token = request.headers[Devise.api.config.authorization.key]
67
+ unless token.blank?
68
+ token = begin
69
+ token.gsub(/^#{Devise.api.config.authorization.scheme} /,
70
+ '')
71
+ rescue StandardError
72
+ token
73
+ end
74
+ end
75
+ token
76
+ end
77
+
78
+ def find_devise_api_token
79
+ case Devise.api.config.authorization.location
80
+ when :header
81
+ extract_devise_api_token_from_headers
82
+ when :params
83
+ extract_devise_api_token_from_params
84
+ when :both
85
+ extract_devise_api_token_from_params || extract_devise_api_token_from_headers
86
+ else
87
+ raise ArgumentError, 'Invalid authorization location, must be :header, :params or :both'
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require 'rails/generators/active_model'
6
+
7
+ module Devise
8
+ module Api
9
+ module Generators
10
+ class InstallGenerator < ::Rails::Generators::Base
11
+ include ::Rails::Generators::Migration
12
+ source_root File.expand_path('templates', __dir__)
13
+ desc 'Generates a migration to add the required fields to the your devise model'
14
+ namespace 'devise_api:install'
15
+
16
+ def install
17
+ migration_template(
18
+ 'migration.rb.erb',
19
+ 'db/migrate/create_devise_api_tables.rb',
20
+ migration_version: migration_version
21
+ )
22
+
23
+ copy_file locale_source, locale_destination
24
+ end
25
+
26
+ def self.next_migration_number(path)
27
+ ActiveRecord::Generators::Base.next_migration_number(path)
28
+ end
29
+
30
+ private
31
+
32
+ def locale_source
33
+ File.expand_path('../../../../config/locales/en.yml', __dir__)
34
+ end
35
+
36
+ def locale_destination
37
+ 'config/locales/devise_api.en.yml'
38
+ end
39
+
40
+ def migration_version
41
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
42
+ end
43
+
44
+ def primary_key_type
45
+ fallback = :integer
46
+
47
+ begin
48
+ ActiveRecord::Base.connection.supports_pgcrypto_uuid? ? :uuid : fallback
49
+ rescue StandardError
50
+ fallback
51
+ end
52
+ end
53
+
54
+ def table_defaults_for_primary_key_type
55
+ return ', type: :uuid' if primary_key_type == :uuid
56
+
57
+ ''
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDeviseApiTables < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :devise_api_tokens<%= table_defaults_for_primary_key_type %> do |t|
6
+ t.belongs_to :resource_owner, null: false<%= table_defaults_for_primary_key_type %>, polymorphic: true, index: true
7
+ t.string :access_token, null: false, index: true
8
+ t.string :refresh_token, null: true, index: true
9
+ t.integer :expires_in, null: false
10
+ t.datetime :revoked_at, null: true
11
+ t.string :previous_refresh_token, null: true, index: true
12
+
13
+ t.timestamps
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ module Api
5
+ module Rails
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace Devise::Api
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ module Routing
5
+ class Mapper
6
+ protected
7
+
8
+ def devise_api(mapping, controllers)
9
+ controller = controllers.fetch(:tokens, 'devise/api/tokens')
10
+ path = mapping.path_names.fetch(:tokens, 'tokens')
11
+
12
+ resource :tokens, only: [], controller: controller, path: path do
13
+ collection do
14
+ post :revoke, as: :revoke
15
+ post :refresh, as: :refresh
16
+ post :sign_up, as: :sign_up
17
+ post :sign_in, as: :sign_in
18
+ get :info, as: :info
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ module Api
5
+ module Responses
6
+ class ErrorResponse
7
+ attr_reader :request, :error, :record, :resource_class
8
+
9
+ ERROR_TYPES = %i[
10
+ invalid_token
11
+ expired_token
12
+ expired_refresh_token
13
+ revoked_token
14
+ refresh_token_disabled
15
+ invalid_refresh_token
16
+ invalid_email
17
+ invalid_resource_owner
18
+ resource_owner_create_error
19
+ devise_api_token_create_error
20
+ devise_api_token_revoke_error
21
+ invalid_authentication
22
+ ].freeze
23
+
24
+ ERROR_TYPES.each do |error_type|
25
+ method_name = error_type.end_with?('_error') ? error_type : "#{error_type}_error"
26
+
27
+ define_method("#{method_name}?") do
28
+ error.eql?(error_type)
29
+ end
30
+ end
31
+
32
+ def initialize(request, error:, record: nil, resource_class: nil)
33
+ @request = request
34
+ @error = error
35
+ @record = record
36
+ @resource_class = resource_class
37
+ end
38
+
39
+ def body
40
+ {
41
+ error: error,
42
+ error_description: error_description,
43
+ lockable: devise_lockable_info,
44
+ confirmable: devise_confirmable_info
45
+ }.compact
46
+ end
47
+
48
+ def status
49
+ return :unauthorized if unauthorized_status?
50
+ return :bad_request if bad_request_status?
51
+
52
+ :unprocessable_entity
53
+ end
54
+
55
+ private
56
+
57
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
58
+ def error_description
59
+ return [I18n.t("devise.api.error_response.#{error}")] if record.blank?
60
+ if invalid_authentication_error? && devise_lockable_info.present? && record.access_locked?
61
+ return [I18n.t('devise.api.error_response.lockable.locked')]
62
+ end
63
+ if invalid_authentication_error? && devise_confirmable_info.present? && !record.confirmed?
64
+ return [I18n.t('devise.api.error_response.confirmable.unconfirmed')]
65
+ end
66
+ return [I18n.t('devise.api.error_response.invalid_authentication')] if invalid_authentication_error?
67
+
68
+ record.errors.full_messages
69
+ end
70
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
71
+
72
+ def devise_lockable_info
73
+ unless resource_class.present? &&
74
+ resource_class.supported_devise_modules.lockable? &&
75
+ invalid_authentication_error?
76
+ return nil
77
+ end
78
+
79
+ unlock_at = record.access_locked? ? record.locked_at + ::Devise.unlock_in : nil
80
+
81
+ {
82
+ locked: record.access_locked?,
83
+ max_attempts: ::Devise.maximum_attempts,
84
+ failed_attemps: record.failed_attempts,
85
+ locked_at: record.locked_at,
86
+ unlock_at: unlock_at
87
+ }.compact
88
+ end
89
+
90
+ def devise_confirmable_info
91
+ unless resource_class.present? &&
92
+ resource_class.supported_devise_modules.confirmable? &&
93
+ invalid_authentication_error?
94
+ return nil
95
+ end
96
+
97
+ {
98
+ confirmed: record.confirmed?,
99
+ confirmation_sent_at: record.confirmed? ? nil : record.confirmation_sent_at
100
+ }.compact
101
+ end
102
+
103
+ def unauthorized_status?
104
+ invalid_token_error? ||
105
+ expired_token_error? ||
106
+ expired_refresh_token_error? ||
107
+ revoked_token_error? ||
108
+ invalid_authentication_error?
109
+ end
110
+
111
+ def bad_request_status?
112
+ invalid_email_error? ||
113
+ invalid_refresh_token_error? ||
114
+ refresh_token_disabled_error? ||
115
+ invalid_resource_owner_error?
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ module Api
5
+ module Responses
6
+ class TokenResponse
7
+ attr_reader :request, :token, :action, :resource_owner
8
+
9
+ ACTIONS = %i[
10
+ sign_in
11
+ sign_up
12
+ refresh
13
+ revoke
14
+ info
15
+ ].freeze
16
+
17
+ ACTIONS.each do |act|
18
+ define_method("#{act}_action?") do
19
+ action.eql?(act)
20
+ end
21
+ end
22
+
23
+ def initialize(request, token:, action:)
24
+ @request = request
25
+ @token = token
26
+ @action = action
27
+ @resource_owner = token&.resource_owner
28
+ end
29
+
30
+ def body
31
+ return {} if revoke_action?
32
+ return signed_up_body if sign_up_action?
33
+ return info_body if info_action?
34
+
35
+ default_body
36
+ end
37
+
38
+ def default_body
39
+ {
40
+ token: token.access_token,
41
+ refresh_token: Devise.api.config.refresh_token.enabled ? token.refresh_token : nil,
42
+ expires_in: token.expires_in,
43
+ token_type: ::Devise.api.config.authorization.scheme,
44
+ resource_owner: {
45
+ id: resource_owner.id,
46
+ email: resource_owner.email,
47
+ created_at: resource_owner.created_at,
48
+ updated_at: resource_owner.updated_at
49
+ }
50
+ }.compact
51
+ end
52
+
53
+ def status
54
+ return :created if sign_up_action?
55
+ return :no_content if revoke_action?
56
+
57
+ :ok
58
+ end
59
+
60
+ private
61
+
62
+ def signed_up_body
63
+ return default_body unless resource_owner.class.supported_devise_modules.confirmable?
64
+
65
+ message = resource_owner.confirmed? ? nil : I18n.t('devise.api.registerable.signed_up_but_unconfirmed')
66
+
67
+ default_body.merge(confirmable: { confirmed: resource_owner.confirmed?, message: message }.compact)
68
+ end
69
+
70
+ def info_body
71
+ default_body[:resource_owner]
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end