Skip to content

Commit 22c8b5b

Browse files
committed
Check reset password token mutation
1 parent 42cbd8c commit 22c8b5b

8 files changed

Lines changed: 156 additions & 5 deletions

File tree

app/controllers/graphql_devise/graphql_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def auth
1515
GraphqlDevise::Schema.execute(params[:query], execute_params(params))
1616
end
1717

18-
render json: result
18+
render json: result unless performed?
1919
end
2020

2121
attr_accessor :client_id, :token, :resource

app/graphql/graphql_devise/mutations/base.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def controller
3131
context[:controller]
3232
end
3333

34+
def set_auth_headers(resource)
35+
auth_headers = resource.create_new_auth_token
36+
response.headers.merge!(auth_headers)
37+
end
38+
3439
def resource_class
3540
context[:resource_class]
3641
end
@@ -39,6 +44,10 @@ def recoverable_enabled?
3944
resource_class.devise_modules.include?(:recoverable)
4045
end
4146

47+
def confirmable_enabled?
48+
resource_class.devise_modules.include?(:confirmable)
49+
end
50+
4251
def current_resource
4352
context[:current_resource]
4453
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module GraphqlDevise
2+
module Mutations
3+
class CheckPasswordToken < Base
4+
argument :reset_password_token, String, required: true
5+
argument :redirect_url, String, required: false
6+
7+
def resolve(reset_password_token:, redirect_url: nil)
8+
resource = resource_class.with_reset_password_token(reset_password_token)
9+
raise_user_error(I18n.t('graphql_devise.passwords.reset_token_not_found')) if resource.blank?
10+
11+
if resource.reset_password_period_valid?
12+
token_info = client_and_token(resource.create_token)
13+
14+
resource.skip_confirmation! if confirmable_enabled? && !resource.confirmed_at
15+
resource.allow_password_change = true if recoverable_enabled?
16+
17+
resource.save!
18+
19+
yield resource if block_given?
20+
21+
redirect_header_options = { reset_password: true }
22+
redirect_headers = controller.send(
23+
:build_redirect_headers,
24+
token_info.fetch(:token),
25+
token_info.fetch(:client_id),
26+
redirect_header_options
27+
)
28+
29+
if redirect_url.present?
30+
controller.redirect_to(resource.build_auth_url(redirect_url, redirect_headers))
31+
else
32+
set_auth_headers(resource)
33+
end
34+
35+
{ authenticable: resource }
36+
else
37+
raise_user_error(I18n.t('graphql_devise.passwords.reset_token_expired'))
38+
end
39+
end
40+
41+
private
42+
43+
def client_and_token(token)
44+
if Gem::Version.new(DeviseTokenAuth::VERSION) <= Gem::Version.new('1.1.0')
45+
{ client_id: token.first, token: token.last }
46+
else
47+
{ client_id: token.client, token: token.token }
48+
end
49+
end
50+
end
51+
end
52+
end

config/locales/en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ en:
88
update_password_error: "Unable to update user password"
99
missing_passwords: "You must fill out the fields labeled 'Password' and 'Password confirmation'."
1010
password_not_required: "This account does not require a password. Sign in using your '%{provider}' account instead."
11+
reset_token_not_found: "No user found for the specified reset token."
12+
reset_token_expired: "Reset password token is no longer valid."
1113
sessions:
1214
bad_credentials: "Invalid login credentials. Please try again."
1315
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"

lib/graphql_devise/rails/routes.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ def mount_graphql_devise_for(resource, opts = {})
1717
GraphqlDevise::Types::AuthenticableType
1818

1919
default_mutations = {
20-
login: GraphqlDevise::Mutations::Login,
21-
logout: GraphqlDevise::Mutations::Logout,
22-
sign_up: GraphqlDevise::Mutations::SignUp,
23-
update_password: GraphqlDevise::Mutations::UpdatePassword
20+
login: GraphqlDevise::Mutations::Login,
21+
logout: GraphqlDevise::Mutations::Logout,
22+
sign_up: GraphqlDevise::Mutations::SignUp,
23+
update_password: GraphqlDevise::Mutations::UpdatePassword,
24+
check_password_token: GraphqlDevise::Mutations::CheckPasswordToken
2425
}.freeze
2526

2627
default_mutations.each do |action, mutation|
@@ -41,6 +42,7 @@ def mount_graphql_devise_for(resource, opts = {})
4142

4243
devise_scope mapping_name.to_sym do
4344
post "#{path}/graphql_auth", to: 'graphql_devise/graphql#auth'
45+
get "#{path}/graphql_auth", to: 'graphql_devise/graphql#auth'
4446
end
4547
end
4648
end

spec/rails_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@
3535

3636
config.include(Requests::JsonHelpers, type: :request)
3737
config.include(Requests::AuthHelpers, type: :request)
38+
config.include(ActiveSupport::Testing::TimeHelpers)
3839
end
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe 'Check Password Token Requests' do
4+
include_context 'with graphql query request'
5+
6+
let(:user) { create(:user, :confirmed) }
7+
let(:redirect_url) { 'https://google.com' }
8+
let(:query) do
9+
<<-GRAPHQL
10+
mutation {
11+
userCheckPasswordToken(
12+
resetPasswordToken: "#{token}",
13+
redirectUrl: "#{redirect_url}"
14+
) {
15+
authenticable { email }
16+
}
17+
}
18+
GRAPHQL
19+
end
20+
21+
context 'when reset password token is valid' do
22+
let(:token) { user.send(:set_reset_password_token) }
23+
24+
context 'when redirect_url is not provided' do
25+
let(:redirect_url) { nil }
26+
27+
it 'returns authenticable and credentials in the headers' do
28+
get_request
29+
30+
expect(response).to include_auth_headers
31+
expect(json_response[:data][:userCheckPasswordToken]).to match(
32+
authenticable: { email: user.email }
33+
)
34+
end
35+
end
36+
37+
context 'when redirect url is provided' do
38+
it 'redirects to redirect url' do
39+
expect do
40+
get_request
41+
42+
user.reload
43+
end.to change { user.tokens.keys.count }.from(0).to(1).and(
44+
change(user, :allow_password_change).from(false).to(true)
45+
)
46+
47+
expect(response).to redirect_to %r{\Ahttps://google.com}
48+
expect(response.body).to include("client=#{user.reload.tokens.keys.first}")
49+
expect(response.body).to include('access-token=')
50+
expect(response.body).to include('uid=')
51+
expect(response.body).to include('expiry=')
52+
end
53+
end
54+
55+
context 'when token has expired' do
56+
it 'returns an expired token error' do
57+
travel_to 10.hours.ago do
58+
token
59+
end
60+
61+
get_request
62+
63+
expect(json_response[:errors]).to contain_exactly(
64+
hash_including(message: 'Reset password token is no longer valid.', extensions: { code: 'USER_ERROR' })
65+
)
66+
end
67+
end
68+
end
69+
70+
context 'when reset password token is not found' do
71+
let(:token) { user.send(:set_reset_password_token) + 'invalid' }
72+
73+
it 'redirects to redirect url' do
74+
get_request
75+
76+
expect(json_response[:errors]).to contain_exactly(
77+
hash_including(message: 'No user found for the specified reset token.', extensions: { code: 'USER_ERROR' })
78+
)
79+
end
80+
end
81+
end

spec/support/contexts/graphql_request.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@
1212
def post_request
1313
post '/api/v1/graphql_auth', *graphql_params
1414
end
15+
16+
def get_request
17+
get '/api/v1/graphql_auth', *graphql_params
18+
end
1519
end

0 commit comments

Comments
 (0)