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
@@ -8,27 +8,33 @@ require 'rails/generators/active_record'
|
|
8
8
|
#
|
9
9
|
# @private
|
10
10
|
#
|
11
|
-
|
12
|
-
|
11
|
+
module ApiBlocks
|
12
|
+
module Doorkeeper
|
13
|
+
module Invitations
|
14
|
+
class MigrationGenerator < ::Rails::Generators::Base
|
15
|
+
include ::Rails::Generators::Migration
|
13
16
|
|
14
|
-
|
15
|
-
|
17
|
+
source_root File.expand_path('templates', __dir__)
|
18
|
+
desc 'Installs doorkeeper invitations api migrations'
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
def install
|
21
|
+
migration_template(
|
22
|
+
'migration.rb.erb',
|
23
|
+
'db/migrate/add_invitation_uri_to_doorkeeper_applications.rb',
|
24
|
+
migration_version: migration_version
|
25
|
+
)
|
26
|
+
end
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
+
def self.next_migration_number(dirname)
|
29
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
30
|
+
end
|
28
31
|
|
29
|
-
|
32
|
+
private
|
30
33
|
|
31
|
-
|
32
|
-
|
34
|
+
def migration_version
|
35
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
33
39
|
end
|
34
40
|
end
|
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# ApiBlocks::Doorkeeper::Passwords implements an API reset password workflow.
|
4
|
-
module ApiBlocks
|
5
|
-
|
4
|
+
module ApiBlocks
|
5
|
+
module Doorkeeper
|
6
|
+
module Passwords
|
7
|
+
extend ActiveSupport::Autoload
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
autoload :Controller
|
10
|
+
autoload :User
|
11
|
+
autoload :Application
|
12
|
+
end
|
13
|
+
end
|
10
14
|
end
|
@@ -8,10 +8,16 @@
|
|
8
8
|
#
|
9
9
|
# @private
|
10
10
|
#
|
11
|
-
module ApiBlocks
|
12
|
-
|
11
|
+
module ApiBlocks
|
12
|
+
module Doorkeeper
|
13
|
+
module Passwords
|
14
|
+
module Application
|
15
|
+
extend ActiveSupport::Concern
|
13
16
|
|
14
|
-
|
15
|
-
|
17
|
+
included do
|
18
|
+
validates :reset_password_uri, "doorkeeper/redirect_uri": true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
16
22
|
end
|
17
23
|
end
|
@@ -53,107 +53,113 @@
|
|
53
53
|
# end
|
54
54
|
# end
|
55
55
|
#
|
56
|
-
module ApiBlocks
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
56
|
+
module ApiBlocks
|
57
|
+
module Doorkeeper
|
58
|
+
module Passwords
|
59
|
+
module Controller
|
60
|
+
extend ActiveSupport::Concern
|
61
|
+
|
62
|
+
included do # rubocop:disable Metrics/BlockLength
|
63
|
+
# Skip pundit after action hooks because there is no authorization to
|
64
|
+
# perform.
|
65
|
+
skip_after_action :verify_authorized
|
66
|
+
skip_after_action :verify_policy_scoped
|
67
|
+
|
68
|
+
# Initialize the reset password workflow, sends a reset password email to
|
69
|
+
# the user.
|
70
|
+
def create
|
71
|
+
user = user_model.send_reset_password_instructions(
|
72
|
+
create_params, application: oauth_application
|
73
|
+
)
|
74
|
+
|
75
|
+
if successfully_sent?(user)
|
76
|
+
render(status: :no_content)
|
77
|
+
else
|
78
|
+
respond_with(user)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Handles the redirection from the email towards the application's
|
83
|
+
# `redirect_uri`.
|
84
|
+
def callback
|
85
|
+
query = {
|
86
|
+
reset_password_token: params[:reset_password_token]
|
87
|
+
}.to_query
|
88
|
+
|
89
|
+
redirect_to(
|
90
|
+
"#{oauth_application.reset_password_uri}?#{query}"
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Updates the user password and returns a new Doorkeeper::AccessToken.
|
95
|
+
def update
|
96
|
+
user = user_model.reset_password_by_token(update_params)
|
97
|
+
|
98
|
+
return respond_with(user) unless user.errors.empty?
|
99
|
+
|
100
|
+
user.unlock_access! if unlockable?(user)
|
101
|
+
|
102
|
+
respond_with(Doorkeeper::OAuth::TokenResponse.new(
|
103
|
+
access_token(oauth_application, user)
|
104
|
+
).body)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# Create permitted parameters
|
110
|
+
def create_params
|
111
|
+
params.require(:user).permit(:email)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Update permitted parameters
|
115
|
+
def update_params
|
116
|
+
params.require(:user).permit(
|
117
|
+
:reset_password_token, :password
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Copied over from devise base controller in order to clear user errors if
|
122
|
+
# `Devise.paranoid` is active.
|
123
|
+
def successfully_sent?(user)
|
124
|
+
if Devise.paranoid
|
125
|
+
user.errors.clear
|
126
|
+
true
|
127
|
+
elsif user.errors.empty?
|
128
|
+
true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Copied over from devise base controller in order to determine wether a ser
|
133
|
+
# is unlockable or not.
|
134
|
+
def unlockable?(resource)
|
135
|
+
resource.respond_to?(:unlock_access!) &&
|
136
|
+
resource.respond_to?(:unlock_strategy_enabled?) &&
|
137
|
+
resource.unlock_strategy_enabled?(:email)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns a new access token for this user.
|
141
|
+
def access_token(application, user)
|
142
|
+
Doorkeeper::AccessToken.find_or_create_for(
|
143
|
+
application,
|
144
|
+
user.id,
|
145
|
+
Doorkeeper.configuration.default_scopes,
|
146
|
+
Doorkeeper.configuration.access_token_expires_in,
|
147
|
+
true
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
def oauth_application
|
152
|
+
@oauth_application ||= Doorkeeper::Application.find_by!(
|
153
|
+
uid: params[:client_id]
|
154
|
+
)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns the user model class.
|
158
|
+
def user_model
|
159
|
+
raise 'the method `user_model` must be implemented on your password controller'
|
160
|
+
end
|
161
|
+
end
|
76
162
|
end
|
77
163
|
end
|
78
|
-
|
79
|
-
# Handles the redirection from the email towards the application's
|
80
|
-
# `redirect_uri`.
|
81
|
-
def callback
|
82
|
-
query = {
|
83
|
-
reset_password_token: params[:reset_password_token]
|
84
|
-
}.to_query
|
85
|
-
|
86
|
-
redirect_to(
|
87
|
-
"#{oauth_application.reset_password_uri}?#{query}"
|
88
|
-
)
|
89
|
-
end
|
90
|
-
|
91
|
-
# Updates the user password and returns a new Doorkeeper::AccessToken.
|
92
|
-
def update
|
93
|
-
user = user_model.reset_password_by_token(update_params)
|
94
|
-
|
95
|
-
return respond_with(user) unless user.errors.empty?
|
96
|
-
|
97
|
-
user.unlock_access! if unlockable?(user)
|
98
|
-
|
99
|
-
respond_with(Doorkeeper::OAuth::TokenResponse.new(
|
100
|
-
access_token(oauth_application, user)
|
101
|
-
).body)
|
102
|
-
end
|
103
|
-
|
104
|
-
private
|
105
|
-
|
106
|
-
# Create permitted parameters
|
107
|
-
def create_params
|
108
|
-
params.require(:user).permit(:email)
|
109
|
-
end
|
110
|
-
|
111
|
-
# Update permitted parameters
|
112
|
-
def update_params
|
113
|
-
params.require(:user).permit(
|
114
|
-
:reset_password_token, :password
|
115
|
-
)
|
116
|
-
end
|
117
|
-
|
118
|
-
# Copied over from devise base controller in order to clear user errors if
|
119
|
-
# `Devise.paranoid` is active.
|
120
|
-
def successfully_sent?(user)
|
121
|
-
if Devise.paranoid
|
122
|
-
user.errors.clear
|
123
|
-
true
|
124
|
-
elsif user.errors.empty?
|
125
|
-
true
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# Copied over from devise base controller in order to determine wether a ser
|
130
|
-
# is unlockable or not.
|
131
|
-
def unlockable?(resource)
|
132
|
-
resource.respond_to?(:unlock_access!) &&
|
133
|
-
resource.respond_to?(:unlock_strategy_enabled?) &&
|
134
|
-
resource.unlock_strategy_enabled?(:email)
|
135
|
-
end
|
136
|
-
|
137
|
-
# Returns a new access token for this user.
|
138
|
-
def access_token(application, user)
|
139
|
-
Doorkeeper::AccessToken.find_or_create_for(
|
140
|
-
application,
|
141
|
-
user.id,
|
142
|
-
Doorkeeper.configuration.default_scopes,
|
143
|
-
Doorkeeper.configuration.access_token_expires_in,
|
144
|
-
true
|
145
|
-
)
|
146
|
-
end
|
147
|
-
|
148
|
-
def oauth_application
|
149
|
-
@oauth_application ||= Doorkeeper::Application.find_by!(
|
150
|
-
uid: params[:client_id]
|
151
|
-
)
|
152
|
-
end
|
153
|
-
|
154
|
-
# Returns the user model class.
|
155
|
-
def user_model
|
156
|
-
raise 'the method `user_model` must be implemented on your password controller' # rubocop:disable Metrics/LineLength
|
157
|
-
end
|
158
164
|
end
|
159
165
|
end
|
@@ -8,27 +8,33 @@ require 'rails/generators/active_record'
|
|
8
8
|
#
|
9
9
|
# @private
|
10
10
|
#
|
11
|
-
|
12
|
-
|
11
|
+
module ApiBlocks
|
12
|
+
module Doorkeeper
|
13
|
+
module Passwords
|
14
|
+
class MigrationGenerator < ::Rails::Generators::Base
|
15
|
+
include ::Rails::Generators::Migration
|
13
16
|
|
14
|
-
|
15
|
-
|
17
|
+
source_root File.expand_path('templates', __dir__)
|
18
|
+
desc 'Installs doorkeeper passwords api migrations'
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
def install
|
21
|
+
migration_template(
|
22
|
+
'migration.rb.erb',
|
23
|
+
'db/migrate/add_reset_password_uri_to_doorkeeper_applications.rb',
|
24
|
+
migration_version: migration_version
|
25
|
+
)
|
26
|
+
end
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
+
def self.next_migration_number(dirname)
|
29
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
30
|
+
end
|
28
31
|
|
29
|
-
|
32
|
+
private
|
30
33
|
|
31
|
-
|
32
|
-
|
34
|
+
def migration_version
|
35
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
33
39
|
end
|
34
40
|
end
|
@@ -9,42 +9,46 @@
|
|
9
9
|
# include ApiBlocks::Doorkeeper::Passwords::User
|
10
10
|
# end
|
11
11
|
#
|
12
|
-
module ApiBlocks
|
13
|
-
|
12
|
+
module ApiBlocks
|
13
|
+
module Doorkeeper
|
14
|
+
module Passwords
|
15
|
+
module User
|
16
|
+
extend ActiveSupport::Concern
|
14
17
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
included do
|
19
|
+
# Resets reset password token and send reset password instructions by email.
|
20
|
+
# Returns the token sent in the e-mail.
|
21
|
+
def send_reset_password_instructions(application = nil)
|
22
|
+
token = set_reset_password_token
|
23
|
+
send_reset_password_instructions_notification(token, application)
|
21
24
|
|
22
|
-
|
23
|
-
|
25
|
+
token
|
26
|
+
end
|
24
27
|
|
25
|
-
|
28
|
+
protected
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
def send_reset_password_instructions_notification(token, application = nil)
|
31
|
+
send_devise_notification(
|
32
|
+
:reset_password_instructions, token, application
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
33
36
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
37
|
+
class_methods do
|
38
|
+
# Attempt to find a user by its email. If a record is found, send new
|
39
|
+
# password instructions to it. If user is not found, returns a new user
|
40
|
+
# with an email not found error.
|
41
|
+
# Attributes must contain the user's email
|
42
|
+
def send_reset_password_instructions(attributes = {}, application: nil)
|
43
|
+
recoverable = find_or_initialize_with_errors(
|
44
|
+
reset_password_keys, attributes, :not_found
|
45
|
+
)
|
43
46
|
|
44
|
-
|
45
|
-
|
47
|
+
recoverable.send_reset_password_instructions(application) if recoverable.persisted?
|
48
|
+
recoverable
|
49
|
+
end
|
50
|
+
end
|
46
51
|
end
|
47
|
-
recoverable
|
48
52
|
end
|
49
53
|
end
|
50
54
|
end
|
@@ -36,88 +36,90 @@ require 'dry/validation'
|
|
36
36
|
# end
|
37
37
|
# end
|
38
38
|
#
|
39
|
-
|
40
|
-
|
39
|
+
module ApiBlocks
|
40
|
+
class Interactor
|
41
|
+
include Dry::Transaction
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
class << self
|
44
|
+
attr_accessor :input_schema
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
47
|
+
# Define a contract for the input of this interactor using
|
48
|
+
# `dry-validation`
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
#
|
52
|
+
# class FooInteractor < ApiBlocks::Interactor
|
53
|
+
# input do
|
54
|
+
# schema do
|
55
|
+
# required(:bar).filled
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# step :validate_input!
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
def self.input(&block)
|
63
|
+
@input_schema = Class.new(Dry::Validation::Contract, &block).new
|
64
|
+
end
|
64
65
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
66
|
+
# Call the interactor with its arguments.
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
#
|
70
|
+
# InviteUser.call(
|
71
|
+
# email: "foo@example.com",
|
72
|
+
# first_name: "Foo",
|
73
|
+
# last_name: "Bar"
|
74
|
+
# )
|
75
|
+
#
|
76
|
+
def self.call(*args)
|
77
|
+
new.call(*args)
|
78
|
+
end
|
78
79
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
80
|
+
# Call the interactor with additional step arguments.
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
#
|
84
|
+
# InviteUser.with_step_args(deliver_invitation: [mailer: UserMailer])
|
85
|
+
#
|
86
|
+
def self.with_step_args(*args)
|
87
|
+
new.with_step_args(*args)
|
88
|
+
end
|
88
89
|
|
89
|
-
|
90
|
+
protected
|
90
91
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
92
|
+
# Validates input with the class attribute `schema` if it is
|
93
|
+
# defined.
|
94
|
+
#
|
95
|
+
# Add this step to your interactor if you want to validate its input.
|
96
|
+
#
|
97
|
+
def validate_input!(input)
|
98
|
+
return Success(input) unless self.class.input_schema
|
98
99
|
|
99
|
-
|
100
|
+
result = self.class.input_schema.call(input)
|
100
101
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
102
|
+
if result.success?
|
103
|
+
Success(result.values)
|
104
|
+
else
|
105
|
+
Failure(result)
|
106
|
+
end
|
105
107
|
end
|
106
|
-
end
|
107
108
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
109
|
+
# Wraps the steps inside an AR transaction.
|
110
|
+
#
|
111
|
+
# Add this step to your interactor if you want to wrap its operations inside a
|
112
|
+
# database transaction
|
113
|
+
#
|
114
|
+
def database_transaction!(input)
|
115
|
+
result = nil
|
115
116
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
117
|
+
ActiveRecord::Base.transaction do
|
118
|
+
result = yield(Success(input))
|
119
|
+
raise ActiveRecord::Rollback if result.failure?
|
120
|
+
end
|
120
121
|
|
121
|
-
|
122
|
+
result
|
123
|
+
end
|
122
124
|
end
|
123
125
|
end
|