Skip to content

Commit 5f91785

Browse files
authored
Merge pull request #13 from graphql-devise/create-user-confirmation-query
Create user confirmation query
2 parents 457c093 + a8ebdae commit 5f91785

9 files changed

Lines changed: 209 additions & 17 deletions

File tree

app/graphql/graphql_devise/mutations/sign_up.rb

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@ def resolve(confirm_success_url: nil, config_name: nil, **attrs)
1616
if resource.save
1717
yield resource if block_given?
1818

19-
if confirmable_enabled?(resource) && !resource.confirmed?
20-
# user will require email authentication
19+
if requires_confirmation?(resource)
2120
resource.send_confirmation_instructions(
2221
client_config: config_name,
2322
redirect_url: confirm_success_url
2423
)
2524
end
2625

27-
set_auth_headers(resource) if active_for_authentication?(resource)
26+
set_auth_headers(resource) if resource.active_for_authentication?
2827

2928
{ authenticable: resource }
3029
else
@@ -45,8 +44,8 @@ def confirmable_enabled?(resource)
4544
resource.respond_to?(:confirmed_at)
4645
end
4746

48-
def active_for_authentication?(resource)
49-
resource.active_for_authentication?
47+
def requires_confirmation?(resource)
48+
resource.active_for_authentication? || !resource.confirmed?
5049
end
5150

5251
def provider
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
require 'devise_token_auth/version'
2+
3+
module GraphqlDevise
4+
module Resolvers
5+
class Base < GraphQL::Schema::Resolver
6+
private
7+
8+
def raise_user_error(message)
9+
raise GraphqlDevise::UserError, message
10+
end
11+
12+
def raise_user_error_list(message, errors:)
13+
raise GraphqlDevise::DetailedUserError.new(message, errors: errors)
14+
end
15+
16+
def request
17+
controller.request
18+
end
19+
20+
def response
21+
controller.response
22+
end
23+
24+
def controller
25+
context[:controller]
26+
end
27+
28+
def resource_class
29+
context[:resource_class]
30+
end
31+
32+
def confirmable_enabled?
33+
resource_class.devise_modules.include?(:confirmable)
34+
end
35+
36+
def recoverable_enabled?
37+
resource_class.devise_modules.include?(:recoverable)
38+
end
39+
40+
def current_resource
41+
context[:current_resource]
42+
end
43+
44+
def client
45+
if Gem::Version.new(DeviseTokenAuth::VERSION) <= Gem::Version.new('1.1.0')
46+
controller.client_id
47+
else
48+
controller.token.client if controller.token.present?
49+
end
50+
end
51+
end
52+
end
53+
end
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module GraphqlDevise
2+
module Resolvers
3+
class ConfirmAccount < Base
4+
argument :confirmation_token, String, required: true
5+
argument :redirect_url, String, required: true
6+
7+
def resolve(confirmation_token:, redirect_url:)
8+
resource = resource_class.confirm_by_token(confirmation_token)
9+
10+
if resource.errors.empty?
11+
yield resource if block_given?
12+
13+
redirect_header_options = { account_confirmation_success: true }
14+
15+
redirect_to_link = if controller.signed_in?(resource_name)
16+
signed_in_resource.build_auth_url(
17+
redirect_url,
18+
redirect_headers(
19+
token_and_client(controller.signed_in_resource.create_token),
20+
redirect_header_options
21+
)
22+
)
23+
else
24+
DeviseTokenAuth::Url.generate(redirect_url, redirect_header_options)
25+
end
26+
27+
controller.redirect_to(redirect_to_link)
28+
{ authenticable: resource }
29+
else
30+
raise_user_error(I18n.t('graphql_devise.confirmations.invalid_token'))
31+
end
32+
end
33+
34+
private
35+
36+
def resource_name
37+
resource_class.to_s.underscore.tr('/', '_')
38+
end
39+
40+
def redirect_headers(token_info, options)
41+
controller.send(
42+
:build_redirect_headers,
43+
token_info.fetch(:token),
44+
token_info.fetch(:client_id),
45+
options
46+
)
47+
end
48+
49+
def client_and_token(token)
50+
if Gem::Version.new(DeviseTokenAuth::VERSION) <= Gem::Version.new('1.1.0')
51+
{ client_id: token.first, token: token.last }
52+
else
53+
{ client_id: token.client, token: token.token }
54+
end
55+
end
56+
end
57+
end
58+
end
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
module GraphqlDevise
22
module MailerHelper
3-
def confirmation_query(token:, config:, redirect_url:)
3+
def confirmation_query(resource_name)
4+
name = "#{resource_name.camelize(:lower)}ConfirmAccount"
45
raw = <<-GRAPHQL
5-
confirmAccount($token:ID!,$clientConfig:String,redirect:String!){
6-
userConfirmAccount(token:$token,clientConfig:$clientConfig,redirect:$redirect
7-
){
8-
success,errors
6+
query($token:String!,$redirect:String!){
7+
#{name}(confirmationToken:$token,redirectUrl:$redirect){
8+
email
99
}
10-
}&variables={token:"#{token}",clientConfig:"#{config}",redirect:"#{redirect_url}"}
10+
}
1111
GRAPHQL
12-
ERB::Util.url_encode(raw.gsub("\n", '').gsub(' ', ''))
12+
raw.delete("\n").delete(' ')
1313
end
1414
end
1515
end

app/views/devise/mailer/confirmation_instructions.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
<p><%= t '.confirm_link_msg' %> </p>
44

5-
<p><%= link_to t('.confirm_account_link'), api_v1_graphql_auth_url, query: confirmation_query(token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url']).html_safe %></p>
5+
<p><%= link_to t('.confirm_account_link'), url_for(controller: 'graphql_devise/graphql', action: :auth, query: confirmation_query(@resource.class.to_s).html_safe, variables: {token: @token, redirect: message['redirect-url']}) %></p>

config/locales/en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ en:
1313
sessions:
1414
bad_credentials: "Invalid login credentials. Please try again."
1515
not_confirmed: "A confirmation email was sent to your account at '%{email}'. You must follow the instructions in the email before your account can be activated"
16+
confirmations:
17+
invalid_token: "Invalid confirmation token. Please try again"
1618
mailer:
1719
unlock_instructions:
1820
account_lock_msg: "Your account has been locked due to an excessive number of unsuccessful sign in attempts."

lib/graphql_devise/rails/routes.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module ActionDispatch::Routing
22
class Mapper
33
def mount_graphql_devise_for(resource, opts = {})
44
mutation_options = opts[:mutations] || {}
5+
query_options = opts[:queries] || {}
56

67
path = opts.fetch(:at, '/')
78
mapping_name = resource.underscore.tr('/', '_')
@@ -38,6 +39,24 @@ def mount_graphql_devise_for(resource, opts = {})
3839
GraphqlDevise::Types::MutationType.field("#{mapping_name}_#{action}", mutation: used_mutation)
3940
end
4041

42+
default_queries = {
43+
confirm_account: GraphqlDevise::Resolvers::ConfirmAccount
44+
}
45+
46+
default_queries.each do |action, query|
47+
used_query = if query_options[action].present?
48+
query_options[action]
49+
else
50+
new_query = Class.new(query)
51+
new_query.graphql_name("#{resource}#{action.to_s.camelize(:upper)}")
52+
new_query.type(authenticable_type, null: true)
53+
54+
new_query
55+
end
56+
57+
GraphqlDevise::Types::QueryType.field("#{mapping_name}_#{action}", resolver: used_query)
58+
end
59+
4160
Devise.mailer.send(:add_template_helper, GraphqlDevise::MailerHelper)
4261

4362
devise_scope mapping_name.to_sym do

spec/requests/mutations/sign_up_spec.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
require 'rails_helper'
22

3-
RSpec.describe '' do
3+
RSpec.describe 'Sign Up process' do
44
include_context 'with graphql query request'
55

66
let(:name) { Faker::Name.name }
@@ -34,6 +34,7 @@
3434
)
3535

3636
user = User.last
37+
3738
expect(user).not_to be_active_for_authentication
3839
expect(user.confirmed_at).to be_nil
3940
expect(user.valid_password?(password)).to be_truthy
@@ -44,9 +45,13 @@
4445
}
4546
)
4647

47-
email = ActionMailer::Base.deliveries.last
48-
query = ERB::Util.url_encode("confirmAccount($token:ID!,$clientConfig:String,redirect:String!){userConfirmAccount(token:$token,clientConfig:$clientConfig,redirect:$redirect){success,errors}}&variables={token:\"#{user.confirmation_token}\",clientConfig:\"default\",redirect:\"#{redirect}\"}").html_safe
49-
expect(email.body.encoded).to match(/query="#{query}"/)
48+
email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
49+
link = email.css('a').first
50+
51+
expect do
52+
get link['href']
53+
user.reload
54+
end.to change { user.active_for_authentication? }.to(true)
5055
end
5156
end
5257

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe 'Account confirmation' do
4+
include_context 'with graphql query request'
5+
6+
let(:user) { create(:user, confirmed_at: nil) }
7+
let(:redirect) { Faker::Internet.url }
8+
let(:query) do
9+
<<-GRAPHQL
10+
{
11+
userConfirmAccount(
12+
confirmationToken: "#{token}"
13+
redirectUrl: "#{redirect}"
14+
) {
15+
email
16+
name
17+
}
18+
}
19+
GRAPHQL
20+
end
21+
22+
context 'when confirmation token is correct' do
23+
let(:token) { user.confirmation_token }
24+
25+
before { user.send_confirmation_instructions }
26+
27+
it 'confirms the resource and redirects to the sent url' do
28+
expect do
29+
get_request
30+
user.reload
31+
end.to(change(user, :confirmed_at).from(nil))
32+
33+
expect(response).to redirect_to "#{redirect}?account_confirmation_success=true"
34+
expect(user).to be_active_for_authentication
35+
end
36+
end
37+
38+
context 'when reset password token is not found' do
39+
let(:token) { "#{user.confirmation_token}-invalid" }
40+
41+
it 'does *NOT* confirm the user nor does the redirection' do
42+
expect do
43+
get_request
44+
user.reload
45+
end.not_to(change(user, :confirmed_at).from(nil))
46+
47+
expect(response).not_to be_redirect
48+
expect(json_response[:errors]).to contain_exactly(
49+
hash_including(
50+
message: 'Invalid confirmation token. Please try again',
51+
extensions: { code: 'USER_ERROR' }
52+
)
53+
)
54+
end
55+
end
56+
end

0 commit comments

Comments
 (0)