Skip to content

Commit 88598b8

Browse files
committed
Add sendPasswordResetWithToken mutation
1 parent 8d05630 commit 88598b8

5 files changed

Lines changed: 132 additions & 9 deletions

File tree

app/views/graphql_devise/mailer/reset_password_instructions.html.erb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
<p><%= t('.request_reset_link_msg') %></p>
44

5-
<p><%= link_to t('.password_change_link'), "#{message['schema_url']}?#{password_reset_query(token: @token, redirect_url: message['redirect-url'], resource_name: @resource.class.to_s).to_query}" %></p>
5+
<p>
6+
<% if message['schema_url'].present? %>
7+
<%= link_to t('.password_change_link'), "#{message['schema_url']}?#{password_reset_query(token: @token, redirect_url: message['redirect-url'], resource_name: @resource.class.to_s).to_query}" %>
8+
<% else %>
9+
<%= link_to t('.password_change_link'), "#{message['redirect-url'].to_s}?#{{ reset_password_token: @token }.to_query}" %>
10+
<% end %>
11+
</p>
612

713
<p><%= t('.ignore_mail_msg') %></p>
814
<p><%= t('.no_changes_msg') %></p>

lib/graphql_devise/default_operations/mutations.rb

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,22 @@
55
require 'graphql_devise/mutations/logout'
66
require 'graphql_devise/mutations/resend_confirmation'
77
require 'graphql_devise/mutations/send_password_reset'
8+
require 'graphql_devise/mutations/send_password_reset_with_token'
89
require 'graphql_devise/mutations/sign_up'
910
require 'graphql_devise/mutations/update_password'
1011
require 'graphql_devise/mutations/update_password_with_token'
1112

1213
module GraphqlDevise
1314
module DefaultOperations
1415
MUTATIONS = {
15-
login: { klass: GraphqlDevise::Mutations::Login, authenticatable: true },
16-
logout: { klass: GraphqlDevise::Mutations::Logout, authenticatable: true },
17-
sign_up: { klass: GraphqlDevise::Mutations::SignUp, authenticatable: true },
18-
update_password: { klass: GraphqlDevise::Mutations::UpdatePassword, authenticatable: true },
19-
update_password_with_token: { klass: GraphqlDevise::Mutations::UpdatePasswordWithToken, authenticatable: true },
20-
send_password_reset: { klass: GraphqlDevise::Mutations::SendPasswordReset, authenticatable: false },
21-
resend_confirmation: { klass: GraphqlDevise::Mutations::ResendConfirmation, authenticatable: false }
16+
login: { klass: GraphqlDevise::Mutations::Login, authenticatable: true },
17+
logout: { klass: GraphqlDevise::Mutations::Logout, authenticatable: true },
18+
sign_up: { klass: GraphqlDevise::Mutations::SignUp, authenticatable: true },
19+
update_password: { klass: GraphqlDevise::Mutations::UpdatePassword, authenticatable: true },
20+
update_password_with_token: { klass: GraphqlDevise::Mutations::UpdatePasswordWithToken, authenticatable: true },
21+
send_password_reset: { klass: GraphqlDevise::Mutations::SendPasswordReset, authenticatable: false },
22+
send_password_reset_with_token: { klass: GraphqlDevise::Mutations::SendPasswordResetWithToken, authenticatable: false },
23+
resend_confirmation: { klass: GraphqlDevise::Mutations::ResendConfirmation, authenticatable: false }
2224
}.freeze
2325
end
2426
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/graphql_devise/mutations/update_password_with_token.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class UpdatePasswordWithToken < Base
1010
field :credentials,
1111
GraphqlDevise::Types::CredentialType,
1212
null: true,
13-
description: 'Authentication credentials. Resource must be signed_in in order for credentials to be returned.'
13+
description: 'Authentication credentials. Resource must be signed_in for credentials to be returned.'
1414

1515
def resolve(reset_password_token:, **attrs)
1616
raise_user_error(I18n.t('graphql_devise.passwords.password_recovery_disabled')) unless recoverable_enabled?
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Send Password Reset Requests' do
6+
include_context 'with graphql query request'
7+
8+
let!(:user) { create(:user, :confirmed, email: 'jwinnfield@wallaceinc.com') }
9+
let(:email) { user.email }
10+
let(:redirect_url) { 'https://google.com' }
11+
let(:query) do
12+
<<-GRAPHQL
13+
mutation {
14+
userSendPasswordResetWithToken(
15+
email: "#{email}",
16+
redirectUrl: "#{redirect_url}"
17+
) {
18+
message
19+
}
20+
}
21+
GRAPHQL
22+
end
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+
39+
context 'when params are correct' do
40+
context 'when using the gem schema' do
41+
it 'sends password reset email' do
42+
expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
43+
44+
expect(json_response[:data][:userSendPasswordResetWithToken]).to include(
45+
message: 'You will receive an email with instructions on how to reset your password in a few minutes.'
46+
)
47+
48+
email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
49+
link = email.css('a').first
50+
51+
expect(link['href']).to include(redirect_url + '?reset_password_token')
52+
end
53+
end
54+
end
55+
56+
context 'when email address uses different casing' do
57+
let(:email) { 'jWinnfield@wallaceinc.com' }
58+
59+
it 'honors devise configuration for case insensitive fields' do
60+
expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
61+
expect(json_response[:data][:userSendPasswordResetWithToken]).to include(
62+
message: 'You will receive an email with instructions on how to reset your password in a few minutes.'
63+
)
64+
end
65+
end
66+
67+
context 'when user email is not found' do
68+
let(:email) { 'nothere@gmail.com' }
69+
70+
before { post_request }
71+
72+
it 'returns an error' do
73+
expect(json_response[:errors]).to contain_exactly(
74+
hash_including(message: 'User was not found or was not logged in.', extensions: { code: 'USER_ERROR' })
75+
)
76+
end
77+
end
78+
end

0 commit comments

Comments
 (0)