Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

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

<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>
<p>
<% if message['schema_url'].present? %>
<%= 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}" %>
<% else %>
<%= link_to t('.password_change_link'), "#{message['redirect-url'].to_s}?#{{ reset_password_token: @token }.to_query}" %>
<% end %>
</p>

<p><%= t('.ignore_mail_msg') %></p>
<p><%= t('.no_changes_msg') %></p>
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ en:
registrations:
missing_confirm_redirect_url: "Missing 'confirm_success_url' parameter. Required when confirmable module is enabled."
passwords:
password_recovery_disabled: "You must enable password recovery for this model."
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."
Expand Down
16 changes: 10 additions & 6 deletions lib/graphql_devise/default_operations/mutations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@
require 'graphql_devise/mutations/logout'
require 'graphql_devise/mutations/resend_confirmation'
require 'graphql_devise/mutations/send_password_reset'
require 'graphql_devise/mutations/send_password_reset_with_token'
require 'graphql_devise/mutations/sign_up'
require 'graphql_devise/mutations/update_password'
require 'graphql_devise/mutations/update_password_with_token'

module GraphqlDevise
module DefaultOperations
MUTATIONS = {
login: { klass: GraphqlDevise::Mutations::Login, authenticatable: true },
logout: { klass: GraphqlDevise::Mutations::Logout, authenticatable: true },
sign_up: { klass: GraphqlDevise::Mutations::SignUp, authenticatable: true },
update_password: { klass: GraphqlDevise::Mutations::UpdatePassword, authenticatable: true },
send_password_reset: { klass: GraphqlDevise::Mutations::SendPasswordReset, authenticatable: false },
resend_confirmation: { klass: GraphqlDevise::Mutations::ResendConfirmation, authenticatable: false }
login: { klass: GraphqlDevise::Mutations::Login, authenticatable: true },
logout: { klass: GraphqlDevise::Mutations::Logout, authenticatable: true },
sign_up: { klass: GraphqlDevise::Mutations::SignUp, authenticatable: true },
update_password: { klass: GraphqlDevise::Mutations::UpdatePassword, authenticatable: true },
update_password_with_token: { klass: GraphqlDevise::Mutations::UpdatePasswordWithToken, authenticatable: true },
send_password_reset: { klass: GraphqlDevise::Mutations::SendPasswordReset, authenticatable: false },
send_password_reset_with_token: { klass: GraphqlDevise::Mutations::SendPasswordResetWithToken, authenticatable: false },
resend_confirmation: { klass: GraphqlDevise::Mutations::ResendConfirmation, authenticatable: false }
}.freeze
end
end
37 changes: 37 additions & 0 deletions lib/graphql_devise/mutations/send_password_reset_with_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module GraphqlDevise
module Mutations
class SendPasswordResetWithToken < Base
argument :email, String, required: true
argument :redirect_url, String, required: true

field :message, String, null: false

def resolve(email:, redirect_url:)
check_redirect_url_whitelist!(redirect_url)

resource = find_resource(:email, get_case_insensitive_field(:email, email))

if resource
yield resource if block_given?

resource.send_reset_password_instructions(
email: email,
provider: 'email',
redirect_url: redirect_url,
template_path: ['graphql_devise/mailer']
)

if resource.errors.empty?
{ message: I18n.t('graphql_devise.passwords.send_instructions') }
else
raise_user_error_list(I18n.t('graphql_devise.invalid_resource'), errors: resource.errors.full_messages)
end
else
raise_user_error(I18n.t('graphql_devise.user_not_found'))
end
end
end
end
end
38 changes: 38 additions & 0 deletions lib/graphql_devise/mutations/update_password_with_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module GraphqlDevise
module Mutations
class UpdatePasswordWithToken < Base
argument :password, String, required: true
argument :password_confirmation, String, required: true
argument :reset_password_token, String, required: true

field :credentials,
GraphqlDevise::Types::CredentialType,
null: true,
description: 'Authentication credentials. Resource must be signed_in for credentials to be returned.'

def resolve(reset_password_token:, **attrs)
raise_user_error(I18n.t('graphql_devise.passwords.password_recovery_disabled')) unless recoverable_enabled?

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?
raise_user_error(I18n.t('graphql_devise.passwords.reset_token_expired')) unless resource.reset_password_period_valid?

if resource.update(attrs)
yield resource if block_given?
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.

What kind of black can be passed here? Is there any examples of this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Any block you want, all mutations do the same. Check the specs in this PR for an example.


response_payload = { authenticatable: resource }
response_payload[:credentials] = set_auth_headers(resource) if controller.signed_in?(resource_name)

response_payload
else
raise_user_error_list(
I18n.t('graphql_devise.passwords.update_password_error'),
errors: resource.errors.full_messages
)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Mutations
class ResetAdminPasswordWithToken < GraphqlDevise::Mutations::UpdatePasswordWithToken
field :authenticatable, Types::AdminType, null: false

def resolve(reset_password_token:, **attrs)
super do |admin|
controller.sign_in(admin)
end
end
end
end
3 changes: 2 additions & 1 deletion spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
authenticatable_type: Types::CustomAdminType,
skip: [:sign_up, :check_password_token],
operations: {
confirm_account: Resolvers::ConfirmAdminAccount
confirm_account: Resolvers::ConfirmAdminAccount,
update_password_with_token: Mutations::ResetAdminPasswordWithToken
},
at: '/api/v1/admin/graphql_auth'
)
Expand Down
78 changes: 78 additions & 0 deletions spec/requests/mutations/send_password_reset_with_token_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Send Password Reset Requests' do
include_context 'with graphql query request'

let!(:user) { create(:user, :confirmed, email: 'jwinnfield@wallaceinc.com') }
let(:email) { user.email }
let(:redirect_url) { 'https://google.com' }
let(:query) do
<<-GRAPHQL
mutation {
userSendPasswordResetWithToken(
email: "#{email}",
redirectUrl: "#{redirect_url}"
) {
message
}
}
GRAPHQL
end

context 'when redirect_url is not whitelisted' do
let(:redirect_url) { 'https://not-safe.com' }

it 'returns a not whitelisted redirect url error' do
expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)

expect(json_response[:errors]).to containing_exactly(
hash_including(
message: "Redirect to '#{redirect_url}' not allowed.",
extensions: { code: 'USER_ERROR' }
)
)
end
end

context 'when params are correct' do
context 'when using the gem schema' do
it 'sends password reset email' do
expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)

expect(json_response[:data][:userSendPasswordResetWithToken]).to include(
message: 'You will receive an email with instructions on how to reset your password in a few minutes.'
)

email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
link = email.css('a').first

expect(link['href']).to include(redirect_url + '?reset_password_token')
end
end
end

context 'when email address uses different casing' do
let(:email) { 'jWinnfield@wallaceinc.com' }

it 'honors devise configuration for case insensitive fields' do
expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
expect(json_response[:data][:userSendPasswordResetWithToken]).to include(
message: 'You will receive an email with instructions on how to reset your password in a few minutes.'
)
end
end

context 'when user email is not found' do
let(:email) { 'nothere@gmail.com' }

before { post_request }

it 'returns an error' do
expect(json_response[:errors]).to contain_exactly(
hash_including(message: 'User was not found or was not logged in.', extensions: { code: 'USER_ERROR' })
)
end
end
end
117 changes: 117 additions & 0 deletions spec/requests/mutations/update_password_with_token_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Update Password With Token' do
include_context 'with graphql query request'

let(:password) { '12345678' }
let(:password_confirmation) { password }

context 'when using the user model' do
let(:user) { create(:user, :confirmed) }
let(:query) do
<<-GRAPHQL
mutation {
userUpdatePasswordWithToken(
resetPasswordToken: "#{token}",
password: "#{password}",
passwordConfirmation: "#{password_confirmation}"
) {
authenticatable { email }
credentials { accessToken }
}
}
GRAPHQL
end

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

it 'updates the password' do
expect do
post_request
user.reload
end.to change(user, :encrypted_password)

expect(json_response[:data][:userUpdatePasswordWithToken][:credentials]).to be_nil
expect(json_response[:data][:userUpdatePasswordWithToken][:authenticatable]).to include(email: user.email)
end

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

post_request

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

context 'when password confirmation does not match' do
let(:password_confirmation) { 'does not match' }

it 'returns an error' do
post_request

expect(json_response[:errors]).to contain_exactly(
hash_including(
message: 'Unable to update user password',
extensions: { code: 'USER_ERROR', detailed_errors: ["Password confirmation doesn't match Password"] }
)
)
end
end
end

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

it 'returns an error' do
post_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

context 'when using the admin model' do
let(:admin) { create(:admin, :confirmed) }
let(:query) do
<<-GRAPHQL
mutation {
adminUpdatePasswordWithToken(
resetPasswordToken: "#{token}",
password: "#{password}",
passwordConfirmation: "#{password_confirmation}"
) {
authenticatable { email }
credentials { uid }
}
}
GRAPHQL
end

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

it 'updates the password' do
expect do
post_request
admin.reload
end.to change(admin, :encrypted_password)
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.

WDYT about checking the password is valid instead of encrypted_password being changed?


expect(json_response[:data][:adminUpdatePasswordWithToken]).to include(
credentials: { uid: admin.email },
authenticatable: { email: admin.email }
)
end
end
end
end
2 changes: 1 addition & 1 deletion spec/requests/queries/check_password_token_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
context 'when reset password token is not found' do
let(:token) { user.send(:set_reset_password_token) + 'invalid' }

it 'redirects to redirect url' do
it 'returns an error message' do
get_request

expect(json_response[:errors]).to contain_exactly(
Expand Down