api-blocks 0.7.0 → 0.8.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 +4 -4
- data/lib/api_blocks/blueprinter/association_extractor.rb +63 -60
- data/lib/api_blocks/blueprinter/join_keys.rb +22 -0
- data/lib/api_blocks/controller.rb +57 -54
- data/lib/api_blocks/doorkeeper.rb +6 -4
- data/lib/api_blocks/doorkeeper/invitations.rb +8 -4
- data/lib/api_blocks/doorkeeper/invitations/application.rb +10 -4
- data/lib/api_blocks/doorkeeper/invitations/controller.rb +99 -94
- data/lib/api_blocks/doorkeeper/invitations/migration_generator.rb +23 -17
- data/lib/api_blocks/doorkeeper/passwords.rb +9 -5
- data/lib/api_blocks/doorkeeper/passwords/application.rb +10 -4
- data/lib/api_blocks/doorkeeper/passwords/controller.rb +106 -100
- data/lib/api_blocks/doorkeeper/passwords/migration_generator.rb +23 -17
- data/lib/api_blocks/doorkeeper/passwords/user.rb +33 -29
- data/lib/api_blocks/interactor.rb +73 -71
- data/lib/api_blocks/railtie.rb +12 -10
- data/lib/api_blocks/responder.rb +78 -78
- data/lib/api_blocks/version.rb +1 -1
- metadata +24 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1886cf50c90d96f98f8d0d3157f4a1ef8e0b75fb4654d7954ed3efbd879113a
|
4
|
+
data.tar.gz: 5d108444c19bd159fd1fd5ec6347076e9dc76f25cdeecb269582d0d535ca7ba3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e7431dc25dcf77ecd1d9595114eaec72cf11e549dcdf799cbd8f62dd4b410fceb6395ea2043bac7a58c5ac691ca9ef66178e07bb7277a7dabea4a5567b4c4046
|
7
|
+
data.tar.gz: 81e3a7fa03e212da25370561739a5749450dcc80e3a95d6c81240b586fab4d5cb61fab0baeb6942b0e118561cd57c19e2236cb412a4c58f5016c53f430e2cd63
|
@@ -1,86 +1,89 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'join_keys'
|
4
|
+
|
3
5
|
# Monkey-patch Blueprinter::AssociationExtractor to use `batch-loader` gem in
|
4
6
|
# order to avoid n+1 queries when serializing associations.
|
5
7
|
#
|
6
8
|
# This does not support associations defined using a `proc` as
|
7
9
|
# `options[:blueprint]`
|
8
10
|
#
|
9
|
-
|
10
|
-
|
11
|
+
module Blueprinter
|
12
|
+
class AssociationExtractor < Blueprinter::Extractor
|
13
|
+
alias original_extract extract
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
return original_extract(association_name, object, local_options, options)
|
15
|
-
end
|
15
|
+
def extract(association_name, object, local_options, options = {})
|
16
|
+
return original_extract(association_name, object, local_options, options) unless options.fetch(:batch, true)
|
16
17
|
|
17
|
-
|
18
|
+
association = object.association(association_name)
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
if association.is_a?(ActiveRecord::Associations::HasManyThroughAssociation)
|
21
|
+
return original_extract(association_name, object, local_options, options)
|
22
|
+
end
|
22
23
|
|
23
|
-
|
24
|
-
raise "Cannot load blueprints with a `proc` blueprint option with batch-loader"
|
25
|
-
end
|
24
|
+
raise 'Cannot load blueprints with a `proc` blueprint option with batch-loader' if options[:blueprint].is_a?(Proc)
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
join_key = ::ApiBlocks::Blueprinter::JoinKeys.join_keys(
|
27
|
+
association.reflection
|
28
|
+
)
|
30
29
|
|
31
|
-
|
32
|
-
|
33
|
-
[]
|
34
|
-
end
|
30
|
+
association_id = object.send(join_key.foreign_key)
|
31
|
+
association_klass = association.reflection.class_name
|
35
32
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
33
|
+
default_value = case association
|
34
|
+
when ActiveRecord::Associations::HasManyAssociation
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
|
38
|
+
view = options[:view] || :default
|
39
|
+
scope = if options[:block].present?
|
40
|
+
options[:block].call(object, local_options)
|
41
|
+
else
|
42
|
+
{}
|
43
|
+
end
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
45
|
+
BatchLoader.for(association_id).batch(
|
46
|
+
default_value: default_value,
|
47
|
+
key: [association_name, association_klass, view, options[:blueprint], scope]
|
48
|
+
) do |ids, loader, args|
|
49
|
+
model = association_klass.safe_constantize
|
50
|
+
scope = args[:key].last
|
49
51
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
case association
|
53
|
+
when ActiveRecord::Associations::HasManyAssociation
|
54
|
+
model.where(join_key.key => ids).merge(scope).each do |record|
|
55
|
+
loader.call(record.send(join_key.key)) do |memo|
|
56
|
+
memo << render_blueprint(record, local_options, options)
|
57
|
+
end
|
55
58
|
end
|
59
|
+
when ActiveRecord::Associations::HasOneAssociation
|
60
|
+
model.where(join_key.key => ids).merge(scope).each do |record|
|
61
|
+
loader.call(
|
62
|
+
record.send(join_key.key),
|
63
|
+
render_blueprint(record, local_options, options)
|
64
|
+
)
|
65
|
+
end
|
66
|
+
when ActiveRecord::Associations::BelongsToAssociation
|
67
|
+
model.where(join_key.key => ids).merge(scope).each do |record|
|
68
|
+
loader.call(
|
69
|
+
record.id,
|
70
|
+
render_blueprint(record, local_options, options)
|
71
|
+
)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
raise "unsupported association kind #{association.class.name}"
|
56
75
|
end
|
57
|
-
when ActiveRecord::Associations::HasOneAssociation
|
58
|
-
model.where(join_key.key => ids).merge(scope).each do |record|
|
59
|
-
loader.call(
|
60
|
-
record.send(join_key.key),
|
61
|
-
render_blueprint(record, local_options, options)
|
62
|
-
)
|
63
|
-
end
|
64
|
-
when ActiveRecord::Associations::BelongsToAssociation
|
65
|
-
model.where(join_key.key => ids).merge(scope).each do |record|
|
66
|
-
loader.call(
|
67
|
-
record.id,
|
68
|
-
render_blueprint(record, local_options, options)
|
69
|
-
)
|
70
|
-
end
|
71
|
-
else
|
72
|
-
raise "unsupported association kind #{association.class.name}"
|
73
76
|
end
|
74
77
|
end
|
75
|
-
end
|
76
78
|
|
77
|
-
|
79
|
+
private
|
78
80
|
|
79
|
-
|
80
|
-
|
81
|
+
def render_blueprint(value, local_options, options = {})
|
82
|
+
return default_value(options) if value.nil?
|
81
83
|
|
82
|
-
|
83
|
-
|
84
|
-
|
84
|
+
view = options[:view] || :default
|
85
|
+
blueprint = association_blueprint(options[:blueprint], value)
|
86
|
+
blueprint.prepare(value, view_name: view, local_options: local_options)
|
87
|
+
end
|
85
88
|
end
|
86
89
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/join_keys.rb
|
4
|
+
|
5
|
+
module ApiBlocks
|
6
|
+
module Blueprinter
|
7
|
+
class JoinKeys
|
8
|
+
# Based on https://github.com/MaxLap/activerecord_where_assoc/blob/100318de80dea5f3c177526c3f824fda307ebc04/lib/active_record_where_assoc/active_record_compat.rb
|
9
|
+
if ActiveRecord.gem_version >= Gem::Version.new('6.1.0.rc1')
|
10
|
+
JoinKeys = Struct.new(:key, :foreign_key)
|
11
|
+
def self.join_keys(reflection)
|
12
|
+
JoinKeys.new(reflection.join_primary_key, reflection.join_foreign_key)
|
13
|
+
end
|
14
|
+
|
15
|
+
elsif ActiveRecord.gem_version >= Gem::Version.new('5.1')
|
16
|
+
def self.join_keys(reflection)
|
17
|
+
reflection.join_keys
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# frozen_string_litreal: true
|
2
4
|
|
3
5
|
require 'pundit'
|
@@ -17,74 +19,75 @@ require 'active_support/core_ext/module'
|
|
17
19
|
# pundit_scope :api, :v1
|
18
20
|
# end
|
19
21
|
#
|
20
|
-
module ApiBlocks
|
21
|
-
|
22
|
-
|
23
|
-
included do
|
24
|
-
self.responder = ApiBlocks::Responder
|
22
|
+
module ApiBlocks
|
23
|
+
module Controller
|
24
|
+
extend ActiveSupport::Concern
|
25
25
|
|
26
|
-
|
26
|
+
included do
|
27
|
+
self.responder = ApiBlocks::Responder
|
27
28
|
|
28
|
-
|
29
|
-
rescue_from Pundit::NotAuthorizedError, with: :render_forbidden_error
|
29
|
+
before_action :verify_request_format!
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
after_action :verify_authorized
|
34
|
-
after_action :verify_policy_scoped, except: :create
|
31
|
+
include Pundit
|
32
|
+
rescue_from Pundit::NotAuthorizedError, with: :render_forbidden_error
|
35
33
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
super(api_scope + [scope], policy_scope_class: policy_scope_class)
|
42
|
-
end
|
34
|
+
# Enable pundit after_action hooks to ensure policies are consistently
|
35
|
+
# used.
|
36
|
+
after_action :verify_authorized
|
37
|
+
after_action :verify_policy_scoped, except: :create
|
43
38
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
39
|
+
# Override policy_scope to lookup pundit policies under the `scope`
|
40
|
+
# namespace
|
41
|
+
def policy_scope(scope, policy_scope_class: nil)
|
42
|
+
api_scope = self.class.inherited_pundit_api_scope || []
|
48
43
|
|
49
|
-
|
50
|
-
|
44
|
+
super(api_scope + [scope], policy_scope_class: policy_scope_class)
|
45
|
+
end
|
51
46
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
47
|
+
# Override authorize to lookup pundit policies under the `scope`
|
48
|
+
# namespace
|
49
|
+
def authorize(record, query = nil, policy_class: nil)
|
50
|
+
api_scope = self.class.inherited_pundit_api_scope || []
|
56
51
|
|
57
|
-
|
58
|
-
|
59
|
-
# the inheritance chain of the current class.
|
60
|
-
def inherited_pundit_api_scope
|
61
|
-
ancestors
|
62
|
-
.select { |a| a.respond_to?(:pundit_api_scope) }
|
63
|
-
.find(&:pundit_api_scope)
|
64
|
-
.pundit_api_scope
|
65
|
-
end
|
52
|
+
super(api_scope + [record], query, policy_class: policy_class)
|
53
|
+
end
|
66
54
|
|
67
|
-
|
68
|
-
|
69
|
-
|
55
|
+
handle_api_error Pundit::NotAuthorizedError do |error|
|
56
|
+
[{ detail: error.message }, :forbidden]
|
57
|
+
end
|
70
58
|
end
|
71
59
|
|
72
|
-
|
73
|
-
|
74
|
-
|
60
|
+
class_methods do
|
61
|
+
# Returns the `pundit_api_scope` value that was defined last looking up into
|
62
|
+
# the inheritance chain of the current class.
|
63
|
+
def inherited_pundit_api_scope
|
64
|
+
ancestors
|
65
|
+
.select { |a| a.respond_to?(:pundit_api_scope) }
|
66
|
+
.find(&:pundit_api_scope)
|
67
|
+
.pundit_api_scope
|
68
|
+
end
|
75
69
|
|
70
|
+
# Provide a default scope to pundit's `PolicyFinder`.
|
71
|
+
def pundit_scope(*scope)
|
72
|
+
@pundit_api_scope ||= scope
|
73
|
+
end
|
76
74
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
problem, status =
|
81
|
-
if block_given?
|
82
|
-
yield ex
|
83
|
-
else
|
84
|
-
[{ detail: ex.message }, :ok]
|
85
|
-
end
|
75
|
+
def pundit_api_scope
|
76
|
+
@pundit_api_scope
|
77
|
+
end
|
86
78
|
|
87
|
-
|
79
|
+
# Defines a error handler that returns
|
80
|
+
def handle_api_error(error_class)
|
81
|
+
rescue_from error_class do |ex|
|
82
|
+
problem, status =
|
83
|
+
if block_given?
|
84
|
+
yield ex
|
85
|
+
else
|
86
|
+
[{ detail: ex.message }, :ok]
|
87
|
+
end
|
88
|
+
|
89
|
+
render problem: problem, status: status
|
90
|
+
end
|
88
91
|
end
|
89
92
|
end
|
90
93
|
end
|
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# ApiBlocks::Doorkeeper implements API extensions for doorkeeper.
|
4
|
-
module ApiBlocks
|
5
|
-
|
4
|
+
module ApiBlocks
|
5
|
+
module Doorkeeper
|
6
|
+
extend ActiveSupport::Autoload
|
6
7
|
|
7
|
-
|
8
|
-
|
8
|
+
autoload :Passwords
|
9
|
+
autoload :Invitations
|
10
|
+
end
|
9
11
|
end
|
@@ -1,9 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# ApiBlocks::Doorkeeper::Invitations implements an API invitation workflow.
|
4
|
-
module ApiBlocks
|
5
|
-
|
4
|
+
module ApiBlocks
|
5
|
+
module Doorkeeper
|
6
|
+
module Invitations
|
7
|
+
extend ActiveSupport::Autoload
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
+
autoload :Controller
|
10
|
+
autoload :Application
|
11
|
+
end
|
12
|
+
end
|
9
13
|
end
|
@@ -8,10 +8,16 @@
|
|
8
8
|
#
|
9
9
|
# @private
|
10
10
|
#
|
11
|
-
module ApiBlocks
|
12
|
-
|
11
|
+
module ApiBlocks
|
12
|
+
module Doorkeeper
|
13
|
+
module Invitations
|
14
|
+
module Application
|
15
|
+
extend ActiveSupport::Concern
|
13
16
|
|
14
|
-
|
15
|
-
|
17
|
+
included do
|
18
|
+
validates :invitation_uri, "doorkeeper/redirect_uri": true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
16
22
|
end
|
17
23
|
end
|
@@ -2,101 +2,106 @@
|
|
2
2
|
|
3
3
|
# ApiBlocks::Doorkeeper::Invitations::Controller implements a devise invitable
|
4
4
|
# API controller.
|
5
|
-
module ApiBlocks
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
5
|
+
module ApiBlocks
|
6
|
+
module Doorkeeper
|
7
|
+
module Invitations
|
8
|
+
module Controller
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do # rubocop:disable Metrics/BlockLength
|
12
|
+
skip_after_action :verify_authorized
|
13
|
+
skip_after_action :verify_policy_scoped
|
14
|
+
|
15
|
+
# Initialize a new invitation.
|
16
|
+
def create
|
17
|
+
user = user_model.invite!(
|
18
|
+
create_params, current_user, application: oauth_application
|
19
|
+
)
|
20
|
+
|
21
|
+
return render(status: :no_content) if user.errors.empty?
|
22
|
+
|
23
|
+
respond_with(user)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Renders informations about the invited user.
|
27
|
+
def show
|
28
|
+
user = user_model.find_by_invitation_token(params[:invitation_token], false)
|
29
|
+
|
30
|
+
if user.nil? || !user.persisted?
|
31
|
+
return render(
|
32
|
+
problem: { details: 'invalid invitation token' },
|
33
|
+
status: :bad_request
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
respond_with(user)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Redirects to the application's redirect uri.
|
41
|
+
def callback
|
42
|
+
query = {
|
43
|
+
invitation_token: params[:invitation_token]
|
44
|
+
}.to_query
|
45
|
+
|
46
|
+
redirect_to("#{oauth_application.invitation_uri}?#{query}")
|
47
|
+
end
|
48
|
+
|
49
|
+
# Finalize the invitation.
|
50
|
+
def update
|
51
|
+
user = user_model.accept_invitation!(update_params)
|
52
|
+
|
53
|
+
return respond_with(user) unless user.errors.empty?
|
54
|
+
|
55
|
+
user.unlock_access! if unlockable?(user)
|
56
|
+
|
57
|
+
respond_with(Doorkeeper::OAuth::TokenResponse.new(
|
58
|
+
access_token(oauth_application, user)
|
59
|
+
).body)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def create_params
|
65
|
+
params.require(:user).permit(:email)
|
66
|
+
end
|
67
|
+
|
68
|
+
def update_params
|
69
|
+
params.require(:user).permit(
|
70
|
+
:invitation_token, :password, :password_confirmation
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Copied over from devise base controller in order to determine wether a ser
|
75
|
+
# is unlockable or not.
|
76
|
+
def unlockable?(resource)
|
77
|
+
resource.respond_to?(:unlock_access!) &&
|
78
|
+
resource.respond_to?(:unlock_strategy_enabled?) &&
|
79
|
+
resource.unlock_strategy_enabled?(:email)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns a new access token for this user.
|
83
|
+
def access_token(application, user)
|
84
|
+
Doorkeeper::AccessToken.find_or_create_for(
|
85
|
+
application,
|
86
|
+
user.id,
|
87
|
+
Doorkeeper.configuration.default_scopes,
|
88
|
+
Doorkeeper.configuration.access_token_expires_in,
|
89
|
+
true
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
def oauth_application
|
94
|
+
@oauth_application ||= Doorkeeper::Application.find_by!(
|
95
|
+
uid: params[:client_id]
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the user model class.
|
100
|
+
def user_model
|
101
|
+
raise 'the method `user_model` must be implemented on your invitations controller'
|
102
|
+
end
|
103
|
+
end
|
32
104
|
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 invitations controller' # rubocop:disable Metrics/LineLength
|
100
105
|
end
|
101
106
|
end
|
102
107
|
end
|