Skip to content

Commit b352f99

Browse files
committed
Add resendConfirmationWithToken mutation
1 parent 2566c94 commit b352f99

4 files changed

Lines changed: 187 additions & 3 deletions

File tree

lib/graphql_devise/default_operations/mutations.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'graphql_devise/mutations/login'
55
require 'graphql_devise/mutations/logout'
66
require 'graphql_devise/mutations/resend_confirmation'
7+
require 'graphql_devise/mutations/resend_confirmation_with_token'
78
require 'graphql_devise/mutations/send_password_reset'
89
require 'graphql_devise/mutations/send_password_reset_with_token'
910
require 'graphql_devise/mutations/sign_up'
@@ -24,6 +25,7 @@ module DefaultOperations
2425
send_password_reset: { klass: GraphqlDevise::Mutations::SendPasswordReset, authenticatable: false },
2526
send_password_reset_with_token: { klass: GraphqlDevise::Mutations::SendPasswordResetWithToken, authenticatable: false },
2627
resend_confirmation: { klass: GraphqlDevise::Mutations::ResendConfirmation, authenticatable: false },
28+
resend_confirmation_with_token: { klass: GraphqlDevise::Mutations::ResendConfirmationWithToken, authenticatable: false },
2729
confirm_registration_with_token: { klass: GraphqlDevise::Mutations::ConfirmRegistrationWithToken, authenticatable: true }
2830
}.freeze
2931
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
module GraphqlDevise
4+
module Mutations
5+
class ResendConfirmationWithToken < Base
6+
argument :email, String, required: true
7+
argument :confirm_url, String, required: true
8+
9+
field :message, String, null: false
10+
11+
def resolve(email:, confirm_url:)
12+
check_redirect_url_whitelist!(confirm_url)
13+
14+
resource = find_confirmable_resource(email)
15+
16+
if resource
17+
yield resource if block_given?
18+
19+
if resource.confirmed? && !resource.pending_reconfirmation?
20+
raise_user_error(I18n.t('graphql_devise.confirmations.already_confirmed'))
21+
end
22+
23+
resource.send_confirmation_instructions(
24+
redirect_url: confirm_url,
25+
template_path: ['graphql_devise/mailer']
26+
)
27+
28+
{ message: I18n.t('graphql_devise.confirmations.send_instructions', email: email) }
29+
else
30+
raise_user_error(I18n.t('graphql_devise.confirmations.user_not_found', email: email))
31+
end
32+
end
33+
34+
private
35+
36+
def find_confirmable_resource(email)
37+
email_insensitive = get_case_insensitive_field(:email, email)
38+
resource = find_resource(:unconfirmed_email, email_insensitive) if resource_class.reconfirmable
39+
resource ||= find_resource(:email, email_insensitive)
40+
resource
41+
end
42+
end
43+
end
44+
end

spec/dummy/app/graphql/dummy_schema.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ class DummySchema < GraphQL::Schema
77
public_introspection: true,
88
resource_loaders: [
99
GraphqlDevise::ResourceLoader.new(
10-
'User',
10+
User,
1111
only: [
1212
:login,
1313
:confirm_account,
1414
:send_password_reset,
1515
:resend_confirmation,
16+
:resend_confirmation_with_token,
1617
:check_password_token
1718
]
1819
),
19-
GraphqlDevise::ResourceLoader.new('Guest', only: [:logout]),
20-
GraphqlDevise::ResourceLoader.new('SchemaUser')
20+
GraphqlDevise::ResourceLoader.new(Guest, only: [:logout]),
21+
GraphqlDevise::ResourceLoader.new(SchemaUser)
2122
]
2223
)
2324

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Resend confirmation with token' do
6+
include_context 'with graphql query request'
7+
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(:confirm_url) { 'https://google.com' }
13+
let(:query) do
14+
<<-GRAPHQL
15+
mutation {
16+
userResendConfirmationWithToken(
17+
email:"#{email}",
18+
confirmUrl:"#{confirm_url}"
19+
) {
20+
message
21+
}
22+
}
23+
GRAPHQL
24+
end
25+
26+
context 'when confirm_url is not whitelisted' do
27+
let(:confirm_url) { 'https://not-safe.com' }
28+
29+
it 'returns a not whitelisted confirm 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 '#{confirm_url}' not allowed.",
35+
extensions: { code: 'USER_ERROR' }
36+
)
37+
)
38+
end
39+
end
40+
41+
context 'when params are correct' do
42+
context 'when using the gem schema' do
43+
it 'sends an email to the user with confirmation url and returns a success message' do
44+
expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
45+
expect(json_response[:data][:userResendConfirmationWithToken]).to include(
46+
message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
47+
)
48+
49+
email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
50+
confirm_link = email.css('a').first['href']
51+
confirm_token = confirm_link.match(/\?confirmationToken\=(?<token>.+)\z/)[:token]
52+
53+
expect(User.confirm_by_token(confirm_token)).to eq(user)
54+
end
55+
end
56+
57+
context 'when using a custom schema' do
58+
let(:custom_path) { '/api/v1/graphql' }
59+
60+
it 'sends an email to the user with confirmation url and returns a success message' do
61+
expect { post_request(custom_path) }.to change(ActionMailer::Base.deliveries, :count).by(1)
62+
expect(json_response[:data][:userResendConfirmationWithToken]).to include(
63+
message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
64+
)
65+
66+
email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
67+
confirm_link = email.css('a').first['href']
68+
confirm_token = confirm_link.match(/\?confirmationToken\=(?<token>.+)\z/)[:token]
69+
70+
expect(User.confirm_by_token(confirm_token)).to eq(user)
71+
end
72+
end
73+
74+
context 'when email address uses different casing' do
75+
let(:email) { 'mWallace@wallaceinc.com' }
76+
77+
it 'honors devise configuration for case insensitive fields' do
78+
expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
79+
expect(json_response[:data][:userResendConfirmationWithToken]).to include(
80+
message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
81+
)
82+
end
83+
end
84+
85+
context 'when the user has already been confirmed' do
86+
before { user.confirm }
87+
88+
it 'does *NOT* send an email and raises an error' do
89+
expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
90+
expect(json_response[:data][:userResendConfirmationWithToken]).to be_nil
91+
expect(json_response[:errors]).to contain_exactly(
92+
hash_including(
93+
message: 'Email was already confirmed, please try signing in',
94+
extensions: { code: 'USER_ERROR' }
95+
)
96+
)
97+
end
98+
end
99+
end
100+
101+
context 'when the email was changed' do
102+
let(:confirmed_at) { 2.seconds.ago }
103+
let(:email) { 'new-email@wallaceinc.com' }
104+
let(:new_email) { email }
105+
106+
before do
107+
user.update_with_email(
108+
email: new_email,
109+
schema_url: 'http://localhost/test',
110+
confirmation_success_url: 'https://google.com'
111+
)
112+
end
113+
114+
it 'sends new confirmation email' do
115+
expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
116+
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(new_email)
117+
expect(json_response[:data][:userResendConfirmationWithToken]).to include(
118+
message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
119+
)
120+
end
121+
end
122+
123+
context "when the email isn't in the system" do
124+
let(:email) { 'notthere@gmail.com' }
125+
126+
it 'does *NOT* send an email and raises an error' do
127+
expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
128+
expect(json_response[:data][:userResendConfirmationWithToken]).to be_nil
129+
expect(json_response[:errors]).to contain_exactly(
130+
hash_including(
131+
message: "Unable to find user with email '#{email}'.",
132+
extensions: { code: 'USER_ERROR' }
133+
)
134+
)
135+
end
136+
end
137+
end

0 commit comments

Comments
 (0)