Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/graphql_devise/graphql_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def auth
GraphqlDevise::Schema.execute(params[:query], execute_params(params))
end

render json: result
render json: result unless performed?
end

attr_accessor :client_id, :token, :resource
Expand Down
4 changes: 4 additions & 0 deletions app/graphql/graphql_devise/mutations/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def recoverable_enabled?
resource_class.devise_modules.include?(:recoverable)
end

def confirmable_enabled?
resource_class.devise_modules.include?(:confirmable)
end

def current_resource
context[:current_resource]
end
Expand Down
52 changes: 52 additions & 0 deletions app/graphql/graphql_devise/mutations/check_password_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module GraphqlDevise
module Mutations
class CheckPasswordToken < Base
argument :reset_password_token, String, required: true
argument :redirect_url, String, required: false

def resolve(reset_password_token:, redirect_url: nil)
resource = resource_class.with_reset_password_token(reset_password_token)
raise_user_error(I18n.t('graphql_devise.passwords.reset_token_not_found')) if resource.blank?

if resource.reset_password_period_valid?
token_info = client_and_token(resource.create_token)

resource.skip_confirmation! if confirmable_enabled? && !resource.confirmed_at
resource.allow_password_change = true if recoverable_enabled?

resource.save!

yield resource if block_given?

redirect_header_options = { reset_password: true }
redirect_headers = controller.send(
:build_redirect_headers,
token_info.fetch(:token),
token_info.fetch(:client_id),
redirect_header_options
)

if redirect_url.present?
controller.redirect_to(resource.build_auth_url(redirect_url, redirect_headers))
else
set_auth_headers(resource)
end

{ authenticable: resource }
else
raise_user_error(I18n.t('graphql_devise.passwords.reset_token_expired'))
end
end

private

def client_and_token(token)
if Gem::Version.new(DeviseTokenAuth::VERSION) <= Gem::Version.new('1.1.0')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we can create a method for this check, It is also used in client method on the base mutation

{ client_id: token.first, token: token.last }
else
{ client_id: token.client, token: token.token }
end
end
end
end
end
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ en:
update_password_error: "Unable to update user password"
missing_passwords: "You must fill out the fields labeled 'Password' and 'Password confirmation'."
password_not_required: "This account does not require a password. Sign in using your '%{provider}' account instead."
reset_token_not_found: "No user found for the specified reset token."
reset_token_expired: "Reset password token is no longer valid."
sessions:
bad_credentials: "Invalid login credentials. Please try again."
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"
Expand Down
10 changes: 6 additions & 4 deletions lib/graphql_devise/rails/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ def mount_graphql_devise_for(resource, opts = {})
GraphqlDevise::Types::AuthenticableType

default_mutations = {
login: GraphqlDevise::Mutations::Login,
logout: GraphqlDevise::Mutations::Logout,
sign_up: GraphqlDevise::Mutations::SignUp,
update_password: GraphqlDevise::Mutations::UpdatePassword
login: GraphqlDevise::Mutations::Login,
logout: GraphqlDevise::Mutations::Logout,
sign_up: GraphqlDevise::Mutations::SignUp,
update_password: GraphqlDevise::Mutations::UpdatePassword,
check_password_token: GraphqlDevise::Mutations::CheckPasswordToken
}.freeze

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

devise_scope mapping_name.to_sym do
post "#{path}/graphql_auth", to: 'graphql_devise/graphql#auth'
get "#{path}/graphql_auth", to: 'graphql_devise/graphql#auth'
end
end
end
Expand Down
1 change: 1 addition & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@

config.include(Requests::JsonHelpers, type: :request)
config.include(Requests::AuthHelpers, type: :request)
config.include(ActiveSupport::Testing::TimeHelpers)
end
81 changes: 81 additions & 0 deletions spec/requests/mutations/check_password_token_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require 'rails_helper'

RSpec.describe 'Check Password Token Requests' do
include_context 'with graphql query request'

let(:user) { create(:user, :confirmed) }
let(:redirect_url) { 'https://google.com' }
let(:query) do
<<-GRAPHQL
mutation {
userCheckPasswordToken(
resetPasswordToken: "#{token}",
redirectUrl: "#{redirect_url}"
) {
authenticable { email }
}
}
GRAPHQL
end

context 'when reset password token is valid' do
let(:token) { user.send(:set_reset_password_token) }

context 'when redirect_url is not provided' do
let(:redirect_url) { nil }

it 'returns authenticable and credentials in the headers' do
get_request

expect(response).to include_auth_headers
expect(json_response[:data][:userCheckPasswordToken]).to match(
authenticable: { email: user.email }
)
end
end

context 'when redirect url is provided' do
it 'redirects to redirect url' do
expect do
get_request

user.reload
end.to change { user.tokens.keys.count }.from(0).to(1).and(
change(user, :allow_password_change).from(false).to(true)
)

expect(response).to redirect_to %r{\Ahttps://google.com}
expect(response.body).to include("client=#{user.reload.tokens.keys.first}")
expect(response.body).to include('access-token=')
expect(response.body).to include('uid=')
expect(response.body).to include('expiry=')
end
end

context 'when token has expired' do
it 'returns an expired token error' do
travel_to 10.hours.ago do
token
end

get_request

expect(json_response[:errors]).to contain_exactly(
hash_including(message: 'Reset password token is no longer valid.', extensions: { code: 'USER_ERROR' })
)
end
end
end

context 'when reset password token is not found' do
let(:token) { user.send(:set_reset_password_token) + 'invalid' }

it 'redirects to redirect url' do
get_request

expect(json_response[:errors]).to contain_exactly(
hash_including(message: 'No user found for the specified reset token.', extensions: { code: 'USER_ERROR' })
)
end
end
end
4 changes: 4 additions & 0 deletions spec/support/contexts/graphql_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
def post_request
post '/api/v1/graphql_auth', *graphql_params
end

def get_request
get '/api/v1/graphql_auth', *graphql_params
end
end