Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
14 changes: 8 additions & 6 deletions lib/graphql_devise/default_operations/mutations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@
require 'graphql_devise/mutations/send_password_reset'
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 },
resend_confirmation: { klass: GraphqlDevise::Mutations::ResendConfirmation, authenticatable: false }
}.freeze
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 in order for credentials to be returned.'
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 about Authentication credentials. Resource must be signed_in for credentials to be returned. ?

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.

yes, makes more sense


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
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