api-blocks 0.2.1 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac783dd6db7fa3ce389eda86b7fd62b8e48d440ecf4ed6a9502b1078fcbfcc5f
4
- data.tar.gz: b689fd259ce4ceb3d30cca568133e36660d8d29e9dd228263b2de352b1761d69
3
+ metadata.gz: 66461c9b8a8937195f5481be3ecf649923d9a9534e7cb4d1194d0b31e1325b92
4
+ data.tar.gz: 81ac3b5a4c0f1d5c4ed3b0653d57e1aa6e343cd03dbdf17a3f400248ddbff840
5
5
  SHA512:
6
- metadata.gz: aa854d23bbde6cebf1fd196d1cb61871073567630e2c0500b4fbacd783dfeebc1307a53a1b1ee6e9ef18c2ef3b885eb2ec0984c060403a436d1718d5b7c4431a
7
- data.tar.gz: b993c3868b1950bdea4dd8e130331ef07c3f1331241159d89d36bbfd359cf8bc254df3209dff88e0c08451e84236a553444072e0fc7f6981cb4dfd9fcfb6b39e
6
+ metadata.gz: 0d0d11a13c33b6fbd07a0e4c54e4a36b89cc3680443908d4db4ec76600c4735a822e9f12ed3e081e5d860cb121ae3c7d30cd3fd8849508ca6ddad180c3fbbc8d
7
+ data.tar.gz: b828665074662b5a2c91739efeb3728d502a22c215688ddd0061cce3b0656370c8d539610d103f522112b160bc922a931934fc4993104cf91cdd9425ce7b1d02
@@ -45,17 +45,32 @@ module ApiBlocks::Controller
45
45
  def authorize(record, query = nil)
46
46
  super(self.class.pundit_api_scope + [record], query)
47
47
  end
48
+
49
+ handle_api_error Pundit::NotAuthorizedError do |error|
50
+ [{ detail: error.message }, :forbidden]
51
+ end
52
+
53
+ mattr_accessor :pundit_api_scope, default: []
48
54
  end
49
55
 
50
56
  class_methods do
51
57
  # Provide a default scope to pundit's `PolicyFinder`.
52
58
  def pundit_scope(*scope)
53
- @pundit_api_scope = scope
59
+ self.pundit_api_scope = scope
54
60
  end
55
61
 
56
- # Returns the scope for pundit's `PolicyFinder`.
57
- def pundit_api_scope
58
- @pundit_api_scope || []
62
+ # Defines a error handler that returns
63
+ def handle_api_error(error_class)
64
+ rescue_from error_class do |ex|
65
+ problem, status =
66
+ if block_given?
67
+ yield ex
68
+ else
69
+ [{ detail: ex.message }, :ok]
70
+ end
71
+
72
+ render problem: problem, status: status
73
+ end
59
74
  end
60
75
  end
61
76
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ApiBlocks::Doorkeeper::Invitations::Application adds `invitation_uri`
4
+ # validation to `Doorkeeper::Application`.
5
+ #
6
+ # This module is automatically included on rails application startup if the
7
+ # invitations migrations have been ran.
8
+ #
9
+ # @private
10
+ #
11
+ module ApiBlocks::Doorkeeper::Invitations::Application
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ validates :invitation_uri, "doorkeeper/redirect_uri": true
16
+ end
17
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ApiBlocks::Doorkeeper::Invitations::Controller implements a devise invitable
4
+ # API controller.
5
+ module ApiBlocks::Doorkeeper::Invitations::Controller
6
+ extend ActiveSupport::Concern
7
+
8
+ included do # rubocop:disable Metrics/BlockLength
9
+ skip_after_action :verify_authorized
10
+ skip_after_action :verify_policy_scoped
11
+
12
+ # Initialize a new invitation.
13
+ def create
14
+ user = user_model.invite!(
15
+ create_params, current_user, application: oauth_application,
16
+ )
17
+
18
+ return render(status: :no_content) if user.errors.empty?
19
+
20
+ respond_with(user)
21
+ end
22
+
23
+ # Renders informations about the invited user.
24
+ def show
25
+ user = user_model.find_by_invitation_token(params[:invitation_token], false)
26
+
27
+ if user.nil? || !user.persisted?
28
+ return render(
29
+ problem: { details: "invalid invitation token" },
30
+ status: :bad_request
31
+ )
32
+ end
33
+
34
+ respond_with(user)
35
+ end
36
+
37
+ # Redirects to the application's redirect uri.
38
+ def callback
39
+ query = {
40
+ invitation_token: params[:invitation_token]
41
+ }.to_query
42
+
43
+ redirect_to("#{oauth_application.invitation_uri}?#{query}")
44
+ end
45
+
46
+ # Finalize the invitation.
47
+ def update
48
+ user = user_model.accept_invitation!(update_params)
49
+
50
+ return respond_with(user) unless user.errors.empty?
51
+
52
+ user.unlock_access! if unlockable?(user)
53
+
54
+ respond_with(Doorkeeper::OAuth::TokenResponse.new(
55
+ access_token(oauth_application, user)
56
+ ).body)
57
+ end
58
+
59
+ private
60
+
61
+ def create_params
62
+ params.require(:user).permit(:email)
63
+ end
64
+
65
+ def update_params
66
+ params.require(:user).permit(
67
+ :invitation_token, :password, :password_confirmation
68
+ )
69
+ end
70
+
71
+ # Copied over from devise base controller in order to determine wether a ser
72
+ # is unlockable or not.
73
+ def unlockable?(resource)
74
+ resource.respond_to?(:unlock_access!) &&
75
+ resource.respond_to?(:unlock_strategy_enabled?) &&
76
+ resource.unlock_strategy_enabled?(:email)
77
+ end
78
+
79
+ # Returns a new access token for this user.
80
+ def access_token(application, user)
81
+ Doorkeeper::AccessToken.find_or_create_for(
82
+ application,
83
+ user.id,
84
+ Doorkeeper.configuration.default_scopes,
85
+ Doorkeeper.configuration.access_token_expires_in,
86
+ true
87
+ )
88
+ end
89
+
90
+ def oauth_application
91
+ @oauth_application ||= Doorkeeper::Application.find_by!(
92
+ uid: params[:client_id]
93
+ )
94
+ end
95
+
96
+
97
+ # Returns the user model class.
98
+ def user_model
99
+ raise 'the method `user_model` must be implemented on your password controller' # rubocop:disable Metrics/LineLength
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ # ApiBlocks::Doorkeeper::Invitations::MigrationGenerator implements the Rails
7
+ # generator for doorkeeper invitations api migrations.
8
+ #
9
+ # @private
10
+ #
11
+ class ApiBlocks::Doorkeeper::Invitations::MigrationGenerator < ::Rails::Generators::Base # rubocop:disable Metrics/LineLength
12
+ include ::Rails::Generators::Migration
13
+
14
+ source_root File.expand_path('templates', __dir__)
15
+ desc 'Installs doorkeeper invitations api migrations'
16
+
17
+ def install
18
+ migration_template(
19
+ 'migration.rb.erb',
20
+ 'db/migrate/add_invitation_uri_to_doorkeeper_applications.rb',
21
+ migration_version: migration_version
22
+ )
23
+ end
24
+
25
+ def self.next_migration_number(dirname)
26
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
27
+ end
28
+
29
+ private
30
+
31
+ def migration_version
32
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ApiBlocks::Doorkeeper::Invitations implements an API invitation workflow.
4
+ module ApiBlocks::Doorkeeper::Invitations
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :Controller
8
+ autoload :Application
9
+ end
@@ -65,10 +65,8 @@ module ApiBlocks::Doorkeeper::Passwords::Controller
65
65
  # Initialize the reset password workflow, sends a reset password email to
66
66
  # the user.
67
67
  def create
68
- application = Doorkeeper::Application.find_by!(uid: params[:client_id])
69
-
70
68
  user = user_model.send_reset_password_instructions(
71
- create_params, application: application
69
+ create_params, application: oauth_application
72
70
  )
73
71
 
74
72
  if successfully_sent?(user)
@@ -81,29 +79,26 @@ module ApiBlocks::Doorkeeper::Passwords::Controller
81
79
  # Handles the redirection from the email towards the application's
82
80
  # `redirect_uri`.
83
81
  def callback
84
- application = Doorkeeper::Application.find_by!(uid: params[:client_id])
85
-
86
82
  query = {
87
83
  reset_password_token: params[:reset_password_token]
88
84
  }.to_query
89
85
 
90
86
  redirect_to(
91
- "#{application.reset_password_uri}?#{query}"
87
+ "#{oauth_application.reset_password_uri}?#{query}"
92
88
  )
93
89
  end
94
90
 
95
91
  # Updates the user password and returns a new Doorkeeper::AccessToken.
96
92
  def update
97
- application = Doorkeeper::Application.find_by!(uid: params[:client_id])
98
93
  user = user_model.reset_password_by_token(update_params)
99
94
 
100
- if user.errors.empty?
101
- user.unlock_access! if unlockable?(user)
95
+ return respond_with(user) unless user.errors.empty?
102
96
 
103
- render json: access_token(application, user)
104
- else
105
- respond_with(user)
106
- end
97
+ user.unlock_access! if unlockable?(user)
98
+
99
+ respond_with(Doorkeeper::OAuth::TokenResponse.new(
100
+ access_token(oauth_application, user)
101
+ ).body)
107
102
  end
108
103
 
109
104
  private
@@ -150,6 +145,12 @@ module ApiBlocks::Doorkeeper::Passwords::Controller
150
145
  )
151
146
  end
152
147
 
148
+ def oauth_application
149
+ @oauth_application ||= Doorkeeper::Application.find_by!(
150
+ uid: params[:client_id]
151
+ )
152
+ end
153
+
153
154
  # Returns the user model class.
154
155
  def user_model
155
156
  raise 'the method `user_model` must be implemented on your password controller' # rubocop:disable Metrics/LineLength
@@ -5,4 +5,5 @@ module ApiBlocks::Doorkeeper
5
5
  extend ActiveSupport::Autoload
6
6
 
7
7
  autoload :Passwords
8
+ autoload :Invitations
8
9
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "problem_details-rails"
4
+
3
5
  # ApiBlocks::Railtie implements the Rails integration for ApiBlocks.
4
6
  #
5
7
  # @private
@@ -21,9 +23,24 @@ class ApiBlocks::Railtie < Rails::Railtie
21
23
  ApiBlocks::Doorkeeper::Passwords::Application
22
24
  )
23
25
  end
26
+
27
+ ActiveSupport.on_load(:active_record) do
28
+ # do not load the Doorkeeper::Application extensions if migrations have
29
+ # not been setup.
30
+ invitation_uri = Doorkeeper::Application.columns.find do |col|
31
+ col.name == 'invitation_uri'
32
+ end
33
+
34
+ next unless invitation_uri
35
+
36
+ Doorkeeper::Application.include(
37
+ ApiBlocks::Doorkeeper::Invitations::Application
38
+ )
39
+ end
24
40
  end
25
41
 
26
42
  generators do
27
43
  require_relative 'doorkeeper/passwords/migration_generator'
44
+ require_relative 'doorkeeper/invitations/migration_generator'
28
45
  end
29
46
  end
@@ -10,13 +10,15 @@ require 'dry/monads/result'
10
10
  class ApiBlocks::Responder < ActionController::Responder
11
11
  include Responders::HttpCacheResponder
12
12
 
13
- # Override resource_errors to handle more error kinds
13
+ # Override resource_errors to handle more error kinds and return a status
14
+ # code.
15
+ #
14
16
  def resource_errors
15
17
  case @resource
16
18
  when ApplicationRecord
17
- { errors: @resource.errors }
19
+ [{ errors: @resource.errors }, :unprocessable_entity]
18
20
  when ActiveRecord::RecordInvalid
19
- { errors: @resource.record.errors }
21
+ [{ errors: @resource.record.errors }, :unprocessable_entity]
20
22
  else
21
23
  # propagate the error so it can be handled through the standard rails
22
24
  # error handlers.
@@ -24,6 +26,24 @@ class ApiBlocks::Responder < ActionController::Responder
24
26
  end
25
27
  end
26
28
 
29
+ # Display is just a shortcut to render a resource's errors with the current
30
+ # format using `problem_details` when format is set to JSON.
31
+ #
32
+ def display_errors
33
+ return super unless format == :json
34
+
35
+ errors, status = resource_errors
36
+
37
+ controller.render problem: errors, status: status
38
+ end
39
+
40
+ # All other formats follow the procedure below. First we try to render a
41
+ # template, if the template is not available, we verify if the resource
42
+ # responds to :to_format and display it.
43
+ #
44
+ # In addition, if the resource is a Dry::Monads::Result we unwrap it and
45
+ # assign the failure instead.
46
+ #
27
47
  def to_format
28
48
  return super unless resource.is_a?(Dry::Monads::Result)
29
49
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ApiBlocks
4
4
  # Current version of ApiBlocks
5
- VERSION = '0.2.1'
5
+ VERSION = '0.4.0'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api-blocks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul d'Hubert
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: problem_details-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.2'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: pundit
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -244,6 +258,10 @@ files:
244
258
  - lib/api_blocks.rb
245
259
  - lib/api_blocks/controller.rb
246
260
  - lib/api_blocks/doorkeeper.rb
261
+ - lib/api_blocks/doorkeeper/invitations.rb
262
+ - lib/api_blocks/doorkeeper/invitations/application.rb
263
+ - lib/api_blocks/doorkeeper/invitations/controller.rb
264
+ - lib/api_blocks/doorkeeper/invitations/migration_generator.rb
247
265
  - lib/api_blocks/doorkeeper/passwords.rb
248
266
  - lib/api_blocks/doorkeeper/passwords/application.rb
249
267
  - lib/api_blocks/doorkeeper/passwords/controller.rb