graphql_devise 0.13.3 → 0.14.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +7 -0
  3. data/Appraisals +14 -0
  4. data/CHANGELOG.md +47 -0
  5. data/README.md +35 -22
  6. data/app/models/graphql_devise/concerns/model.rb +6 -6
  7. data/app/views/graphql_devise/mailer/reset_password_instructions.html.erb +7 -1
  8. data/config/locales/en.yml +2 -1
  9. data/docs/usage/reset_password_flow.md +90 -0
  10. data/graphql_devise.gemspec +1 -1
  11. data/lib/graphql_devise/concerns/controller_methods.rb +7 -1
  12. data/lib/graphql_devise/default_operations/mutations.rb +10 -6
  13. data/lib/graphql_devise/mutations/resend_confirmation.rb +15 -5
  14. data/lib/graphql_devise/mutations/send_password_reset.rb +2 -0
  15. data/lib/graphql_devise/mutations/send_password_reset_with_token.rb +37 -0
  16. data/lib/graphql_devise/mutations/sign_up.rb +1 -3
  17. data/lib/graphql_devise/mutations/update_password_with_token.rb +38 -0
  18. data/lib/graphql_devise/resolvers/check_password_token.rb +1 -0
  19. data/lib/graphql_devise/resolvers/confirm_account.rb +2 -0
  20. data/lib/graphql_devise/schema_plugin.rb +12 -10
  21. data/lib/graphql_devise/version.rb +1 -1
  22. data/spec/dummy/app/graphql/mutations/reset_admin_password_with_token.rb +13 -0
  23. data/spec/dummy/config/initializers/devise_token_auth.rb +2 -0
  24. data/spec/dummy/config/routes.rb +2 -1
  25. data/spec/graphql/user_queries_spec.rb +118 -0
  26. data/spec/requests/mutations/additional_mutations_spec.rb +0 -1
  27. data/spec/requests/mutations/resend_confirmation_spec.rb +42 -4
  28. data/spec/requests/mutations/send_password_reset_spec.rb +16 -1
  29. data/spec/requests/mutations/send_password_reset_with_token_spec.rb +78 -0
  30. data/spec/requests/mutations/sign_up_spec.rb +19 -1
  31. data/spec/requests/mutations/update_password_with_token_spec.rb +119 -0
  32. data/spec/requests/queries/check_password_token_spec.rb +16 -1
  33. data/spec/requests/queries/confirm_account_spec.rb +17 -2
  34. data/spec/requests/user_controller_spec.rb +9 -9
  35. data/spec/support/contexts/schema_test.rb +14 -0
  36. metadata +21 -8
@@ -9,15 +9,16 @@ module GraphqlDevise
9
9
  field :message, String, null: false
10
10
 
11
11
  def resolve(email:, redirect_url:)
12
- resource = find_resource(
13
- :email,
14
- get_case_insensitive_field(:email, email)
15
- )
12
+ check_redirect_url_whitelist!(redirect_url)
13
+
14
+ resource = find_confirmable_resource(email)
16
15
 
17
16
  if resource
18
17
  yield resource if block_given?
19
18
 
20
- raise_user_error(I18n.t('graphql_devise.confirmations.already_confirmed')) if resource.confirmed?
19
+ if resource.confirmed? && !resource.pending_reconfirmation?
20
+ raise_user_error(I18n.t('graphql_devise.confirmations.already_confirmed'))
21
+ end
21
22
 
22
23
  resource.send_confirmation_instructions(
23
24
  redirect_url: redirect_url,
@@ -30,6 +31,15 @@ module GraphqlDevise
30
31
  raise_user_error(I18n.t('graphql_devise.confirmations.user_not_found', email: email))
31
32
  end
32
33
  end
34
+
35
+ private
36
+
37
+ def find_confirmable_resource(email)
38
+ email_insensitive = get_case_insensitive_field(:email, email)
39
+ resource = find_resource(:unconfirmed_email, email_insensitive) if resource_class.reconfirmable
40
+ resource ||= find_resource(:email, email_insensitive)
41
+ resource
42
+ end
33
43
  end
34
44
  end
35
45
  end
@@ -9,6 +9,8 @@ module GraphqlDevise
9
9
  field :message, String, null: false
10
10
 
11
11
  def resolve(email:, redirect_url:)
12
+ check_redirect_url_whitelist!(redirect_url)
13
+
12
14
  resource = find_resource(:email, get_case_insensitive_field(:email, email))
13
15
 
14
16
  if resource
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlDevise
4
+ module Mutations
5
+ class SendPasswordResetWithToken < Base
6
+ argument :email, String, required: true
7
+ argument :redirect_url, String, required: true
8
+
9
+ field :message, String, null: false
10
+
11
+ def resolve(email:, redirect_url:)
12
+ check_redirect_url_whitelist!(redirect_url)
13
+
14
+ resource = find_resource(:email, get_case_insensitive_field(:email, email))
15
+
16
+ if resource
17
+ yield resource if block_given?
18
+
19
+ resource.send_reset_password_instructions(
20
+ email: email,
21
+ provider: 'email',
22
+ redirect_url: redirect_url,
23
+ template_path: ['graphql_devise/mailer']
24
+ )
25
+
26
+ if resource.errors.empty?
27
+ { message: I18n.t('graphql_devise.passwords.send_instructions') }
28
+ else
29
+ raise_user_error_list(I18n.t('graphql_devise.invalid_resource'), errors: resource.errors.full_messages)
30
+ end
31
+ else
32
+ raise_user_error(I18n.t('graphql_devise.user_not_found'))
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -22,9 +22,7 @@ module GraphqlDevise
22
22
  raise_user_error(I18n.t('graphql_devise.registrations.missing_confirm_redirect_url'))
23
23
  end
24
24
 
25
- if blacklisted_redirect_url?(redirect_url)
26
- raise_user_error(I18n.t('graphql_devise.registrations.redirect_url_not_allowed', redirect_url: redirect_url))
27
- end
25
+ check_redirect_url_whitelist!(redirect_url)
28
26
 
29
27
  resource.skip_confirmation_notification! if resource.respond_to?(:skip_confirmation_notification!)
30
28
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlDevise
4
+ module Mutations
5
+ class UpdatePasswordWithToken < Base
6
+ argument :password, String, required: true
7
+ argument :password_confirmation, String, required: true
8
+ argument :reset_password_token, String, required: true
9
+
10
+ field :credentials,
11
+ GraphqlDevise::Types::CredentialType,
12
+ null: true,
13
+ description: 'Authentication credentials. Resource must be signed_in for credentials to be returned.'
14
+
15
+ def resolve(reset_password_token:, **attrs)
16
+ raise_user_error(I18n.t('graphql_devise.passwords.password_recovery_disabled')) unless recoverable_enabled?
17
+
18
+ resource = resource_class.with_reset_password_token(reset_password_token)
19
+ raise_user_error(I18n.t('graphql_devise.passwords.reset_token_not_found')) if resource.blank?
20
+ raise_user_error(I18n.t('graphql_devise.passwords.reset_token_expired')) unless resource.reset_password_period_valid?
21
+
22
+ if resource.update(attrs)
23
+ yield resource if block_given?
24
+
25
+ response_payload = { authenticatable: resource }
26
+ response_payload[:credentials] = set_auth_headers(resource) if controller.signed_in?(resource_name)
27
+
28
+ response_payload
29
+ else
30
+ raise_user_error_list(
31
+ I18n.t('graphql_devise.passwords.update_password_error'),
32
+ errors: resource.errors.full_messages
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -27,6 +27,7 @@ module GraphqlDevise
27
27
  )
28
28
 
29
29
  if redirect_url.present?
30
+ check_redirect_url_whitelist!(redirect_url)
30
31
  controller.redirect_to(resource.build_auth_url(redirect_url, built_redirect_headers))
31
32
  else
32
33
  set_auth_headers(resource)
@@ -7,6 +7,8 @@ module GraphqlDevise
7
7
  argument :redirect_url, String, required: true
8
8
 
9
9
  def resolve(confirmation_token:, redirect_url:)
10
+ check_redirect_url_whitelist!(redirect_url)
11
+
10
12
  resource = resource_class.confirm_by_token(confirmation_token)
11
13
 
12
14
  if resource.errors.empty?
@@ -24,13 +24,12 @@ module GraphqlDevise
24
24
  # Authenticate only root level queries
25
25
  return yield unless event == 'execute_field' && path(trace_data).count == 1
26
26
 
27
- field = traced_field(trace_data)
28
- provided_value = authenticate_option(field, trace_data)
29
- context = set_current_resource(context_from_data(trace_data))
27
+ field = traced_field(trace_data)
28
+ auth_required = authenticate_option(field, trace_data)
29
+ context = context_from_data(trace_data)
30
30
 
31
- if !provided_value.nil?
32
- raise_on_missing_resource(context, field) if provided_value
33
- elsif @authenticate_default
31
+ if auth_required
32
+ context = set_current_resource(context)
34
33
  raise_on_missing_resource(context, field)
35
34
  end
36
35
 
@@ -40,9 +39,10 @@ module GraphqlDevise
40
39
  private
41
40
 
42
41
  def set_current_resource(context)
43
- controller = context[:controller]
44
- resource_names = Array(context[:resource_name])
45
- context[:current_resource] = resource_names.find do |resource_name|
42
+ controller = context[:controller]
43
+ resource_names = Array(context[:resource_name])
44
+
45
+ context[:current_resource] ||= resource_names.find do |resource_name|
46
46
  unless Devise.mappings.key?(resource_name)
47
47
  raise(
48
48
  GraphqlDevise::Error,
@@ -88,11 +88,13 @@ module GraphqlDevise
88
88
  end
89
89
 
90
90
  def authenticate_option(field, trace_data)
91
- if trace_data[:context]
91
+ auth_required = if trace_data[:context]
92
92
  field.metadata[:authenticate]
93
93
  else
94
94
  field.graphql_definition.metadata[:authenticate]
95
95
  end
96
+
97
+ auth_required.nil? ? @authenticate_default : auth_required
96
98
  end
97
99
 
98
100
  def reconfigure_warden!
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlDevise
4
- VERSION = '0.13.3'.freeze
4
+ VERSION = '0.14.1'.freeze
5
5
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutations
4
+ class ResetAdminPasswordWithToken < GraphqlDevise::Mutations::UpdatePasswordWithToken
5
+ field :authenticatable, Types::AdminType, null: false
6
+
7
+ def resolve(reset_password_token:, **attrs)
8
+ super do |admin|
9
+ controller.sign_in(admin)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -39,6 +39,8 @@ DeviseTokenAuth.setup do |config|
39
39
 
40
40
  config.default_confirm_success_url = 'https://google.com'
41
41
 
42
+ config.redirect_whitelist = ['https://google.com']
43
+
42
44
  # By default we will use callbacks for single omniauth.
43
45
  # It depends on fields like email, provider and uid.
44
46
  # config.default_callbacks = true
@@ -15,7 +15,8 @@ Rails.application.routes.draw do
15
15
  authenticatable_type: Types::CustomAdminType,
16
16
  skip: [:sign_up, :check_password_token],
17
17
  operations: {
18
- confirm_account: Resolvers::ConfirmAdminAccount
18
+ confirm_account: Resolvers::ConfirmAdminAccount,
19
+ update_password_with_token: Mutations::ResetAdminPasswordWithToken
19
20
  },
20
21
  at: '/api/v1/admin/graphql_auth'
21
22
  )
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Users controller specs' do
6
+ include_context 'with graphql schema test'
7
+
8
+ let(:schema) { DummySchema }
9
+ let(:user) { create(:user, :confirmed) }
10
+ let(:field) { 'privateField' }
11
+ let(:public_message) { 'Field does not require authentication' }
12
+ let(:private_message) { 'Field will always require authentication' }
13
+ let(:private_error) do
14
+ {
15
+ message: "#{field} field requires authentication",
16
+ extensions: { code: 'AUTHENTICATION_ERROR' }
17
+ }
18
+ end
19
+
20
+ describe 'publicField' do
21
+ let(:query) do
22
+ <<-GRAPHQL
23
+ query {
24
+ publicField
25
+ }
26
+ GRAPHQL
27
+ end
28
+
29
+ context 'when using a regular schema' do
30
+ it 'does not require authentication' do
31
+ expect(response[:data][:publicField]).to eq(public_message)
32
+ end
33
+ end
34
+ end
35
+
36
+ describe 'privateField' do
37
+ let(:query) do
38
+ <<-GRAPHQL
39
+ query {
40
+ privateField
41
+ }
42
+ GRAPHQL
43
+ end
44
+
45
+ context 'when using a regular schema' do
46
+ context 'when user is authenticated' do
47
+ let(:resource) { user }
48
+
49
+ it 'allows to perform the query' do
50
+ expect(response[:data][:privateField]).to eq(private_message)
51
+ end
52
+
53
+ context 'when using a SchemaUser' do
54
+ let(:resource) { create(:schema_user, :confirmed) }
55
+
56
+ it 'allows to perform the query' do
57
+ expect(response[:data][:privateField]).to eq(private_message)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ context 'when using an interpreter schema' do
64
+ let(:schema) { InterpreterSchema }
65
+
66
+ context 'when user is authenticated' do
67
+ let(:resource) { user }
68
+
69
+ it 'allows to perform the query' do
70
+ expect(response[:data][:privateField]).to eq(private_message)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ describe 'user' do
77
+ let(:user_data) { { email: user.email, id: user.id } }
78
+ let(:query) do
79
+ <<-GRAPHQL
80
+ query {
81
+ user(id: #{user.id}) {
82
+ id
83
+ email
84
+ }
85
+ }
86
+ GRAPHQL
87
+ end
88
+
89
+ context 'when using a regular schema' do
90
+ context 'when user is authenticated' do
91
+ let(:resource) { user }
92
+
93
+ it 'allows to perform the query' do
94
+ expect(response[:data][:user]).to match(**user_data)
95
+ end
96
+ end
97
+ end
98
+
99
+ context 'when using an interpreter schema' do
100
+ let(:schema) { InterpreterSchema }
101
+
102
+ context 'when user is authenticated' do
103
+ let(:resource) { user }
104
+
105
+ it 'allows to perform the query' do
106
+ expect(response[:data][:user]).to match(**user_data)
107
+ end
108
+ end
109
+
110
+ context 'when user is not authenticated' do
111
+ # Interpreter schema fields are public unless specified otherwise (plugin setting)
112
+ it 'allows to perform the query' do
113
+ expect(response[:data][:user]).to match(**user_data)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -9,7 +9,6 @@ RSpec.describe 'Additional Mutations' do
9
9
  let(:password) { Faker::Internet.password }
10
10
  let(:password_confirmation) { password }
11
11
  let(:email) { Faker::Internet.email }
12
- let(:redirect) { Faker::Internet.url }
13
12
 
14
13
  context 'when using the user model' do
15
14
  let(:query) do
@@ -5,10 +5,11 @@ require 'rails_helper'
5
5
  RSpec.describe 'Resend confirmation' do
6
6
  include_context 'with graphql query request'
7
7
 
8
- let!(:user) { create(:user, confirmed_at: nil, email: 'mwallace@wallaceinc.com') }
9
- let(:email) { user.email }
10
- let(:id) { user.id }
11
- let(:redirect) { Faker::Internet.url }
8
+ let(:confirmed_at) { nil }
9
+ let!(:user) { create(:user, confirmed_at: nil, email: 'mwallace@wallaceinc.com') }
10
+ let(:email) { user.email }
11
+ let(:id) { user.id }
12
+ let(:redirect) { 'https://google.com' }
12
13
  let(:query) do
13
14
  <<-GRAPHQL
14
15
  mutation {
@@ -22,6 +23,21 @@ RSpec.describe 'Resend confirmation' do
22
23
  GRAPHQL
23
24
  end
24
25
 
26
+ context 'when redirect_url is not whitelisted' do
27
+ let(:redirect) { 'https://not-safe.com' }
28
+
29
+ it 'returns a not whitelisted redirect url error' do
30
+ expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
31
+
32
+ expect(json_response[:errors]).to containing_exactly(
33
+ hash_including(
34
+ message: "Redirect to '#{redirect}' not allowed.",
35
+ extensions: { code: 'USER_ERROR' }
36
+ )
37
+ )
38
+ end
39
+ end
40
+
25
41
  context 'when params are correct' do
26
42
  context 'when using the gem schema' do
27
43
  it 'sends an email to the user with confirmation url and returns a success message' do
@@ -98,6 +114,28 @@ RSpec.describe 'Resend confirmation' do
98
114
  end
99
115
  end
100
116
 
117
+ context 'when the email was changed' do
118
+ let(:confirmed_at) { 2.seconds.ago }
119
+ let(:email) { 'new-email@wallaceinc.com' }
120
+ let(:new_email) { email }
121
+
122
+ before do
123
+ user.update_with_email(
124
+ email: new_email,
125
+ schema_url: 'http://localhost/test',
126
+ confirmation_success_url: 'https://google.com'
127
+ )
128
+ end
129
+
130
+ it 'sends new confirmation email' do
131
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
132
+ expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(new_email)
133
+ expect(json_response[:data][:userResendConfirmation]).to include(
134
+ message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
135
+ )
136
+ end
137
+ end
138
+
101
139
  context "when the email isn't in the system" do
102
140
  let(:email) { 'nothere@gmail.com' }
103
141
 
@@ -7,7 +7,7 @@ RSpec.describe 'Send Password Reset Requests' do
7
7
 
8
8
  let!(:user) { create(:user, :confirmed, email: 'jwinnfield@wallaceinc.com') }
9
9
  let(:email) { user.email }
10
- let(:redirect_url) { Faker::Internet.url }
10
+ let(:redirect_url) { 'https://google.com' }
11
11
  let(:query) do
12
12
  <<-GRAPHQL
13
13
  mutation {
@@ -21,6 +21,21 @@ RSpec.describe 'Send Password Reset Requests' do
21
21
  GRAPHQL
22
22
  end
23
23
 
24
+ context 'when redirect_url is not whitelisted' do
25
+ let(:redirect_url) { 'https://not-safe.com' }
26
+
27
+ it 'returns a not whitelisted redirect url error' do
28
+ expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
29
+
30
+ expect(json_response[:errors]).to containing_exactly(
31
+ hash_including(
32
+ message: "Redirect to '#{redirect_url}' not allowed.",
33
+ extensions: { code: 'USER_ERROR' }
34
+ )
35
+ )
36
+ end
37
+ end
38
+
24
39
  context 'when params are correct' do
25
40
  context 'when using the gem schema' do
26
41
  it 'sends password reset email' do