api-blocks 0.2.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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