diff --git a/README.md b/README.md index 46992d4d..f697ffa8 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,9 @@ is `User`. 1. userSignUp 1. userUpdatePassword 1. userSendPasswordReset +1. `userResendConfirmation(email: String!, redirectUrl: String!): UserResendConfirmationPayload` + The `UserResendConfirmationPayload` will return the `authenticable` resource that was sent the confirmation instructions but also has a `message: String!` that can be used to notify a user what to do after the instructions were sent to them and a `success: Boolean!` to indicate success. #### Queries 1. userConfirmAccount 1. userCheckPasswordToken diff --git a/app/graphql/graphql_devise/mutations/resend_confirmation.rb b/app/graphql/graphql_devise/mutations/resend_confirmation.rb new file mode 100644 index 00000000..f9c591ae --- /dev/null +++ b/app/graphql/graphql_devise/mutations/resend_confirmation.rb @@ -0,0 +1,32 @@ +module GraphqlDevise + module Mutations + class ResendConfirmation < Base + argument :email, String, required: true, prepare: ->(email, _) { email.downcase } + argument :redirect_url, String, required: true + + field :message, String, null: false + + def resolve(email:, redirect_url:) + resource = controller.find_resource(:uid, email) + + if resource + yield resource if block_given? + + raise_user_error(I18n.t('errors.messages.already_confirmed')) if resource.confirmed? + + resource.send_confirmation_instructions({ + redirect_url: redirect_url, + template_path: ['graphql_devise/mailer'] + }) + + { + authenticable: resource, + message: I18n.t('graphql_devise.confirmations.send_instructions', email: email) + } + else + raise_user_error(I18n.t('graphql_devise.confirmations.user_not_found', email: email)) + end + end + end + end +end diff --git a/app/views/graphql_devise/mailer/confirmation_instructions.html.erb b/app/views/graphql_devise/mailer/confirmation_instructions.html.erb index bc4e4b8b..e2e75806 100644 --- a/app/views/graphql_devise/mailer/confirmation_instructions.html.erb +++ b/app/views/graphql_devise/mailer/confirmation_instructions.html.erb @@ -1,5 +1,5 @@

<%= t(:welcome).capitalize + ' ' + @email %>!

-

<%= t '.confirm_link_msg' %>

+

<%= t '.confirm_link_msg' %>

<%= link_to t('.confirm_account_link'), url_for(controller: 'graphql_devise/graphql', action: :auth, **confirmation_query(resource_name: @resource.class.to_s, redirect_url: message['redirect-url'], token: @token)) %>

diff --git a/config/locales/en.yml b/config/locales/en.yml index d2155c4d..217525d2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -19,6 +19,14 @@ en: 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" confirmations: invalid_token: "Invalid confirmation token. Please try again" + user_not_found: "Unable to find user with email '%{email}'." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." mailer: + confirmation_instructions: + confirm_link_msg: "You can confirm your account email through the link below:" + confirm_account_link: "Confirm my account" unlock_instructions: account_lock_msg: "Your account has been locked due to an excessive number of unsuccessful sign in attempts." + errors: + messages: + already_confirmed: "Email was already confirmed, please try signing in" diff --git a/lib/graphql_devise/rails/routes.rb b/lib/graphql_devise/rails/routes.rb index 246a2b9b..ad4c7de1 100644 --- a/lib/graphql_devise/rails/routes.rb +++ b/lib/graphql_devise/rails/routes.rb @@ -14,7 +14,8 @@ def mount_graphql_devise_for(resource, opts = {}) logout: GraphqlDevise::Mutations::Logout, sign_up: GraphqlDevise::Mutations::SignUp, update_password: GraphqlDevise::Mutations::UpdatePassword, - send_password_reset: GraphqlDevise::Mutations::SendPasswordReset + send_password_reset: GraphqlDevise::Mutations::SendPasswordReset, + resend_confirmation: GraphqlDevise::Mutations::ResendConfirmation }.freeze default_queries = { confirm_account: GraphqlDevise::Resolvers::ConfirmAccount, diff --git a/spec/dummy/config/locales/devise.en.yml b/spec/dummy/config/locales/devise.en.yml index ca3d6199..3c527373 100644 --- a/spec/dummy/config/locales/devise.en.yml +++ b/spec/dummy/config/locales/devise.en.yml @@ -55,7 +55,6 @@ en: unlocked: "Your account has been unlocked successfully. Please sign in to continue." errors: messages: - already_confirmed: "was already confirmed, please try signing in" confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" expired: "has expired, please request a new one" not_found: "not found" diff --git a/spec/requests/mutations/resend_confirmation_spec.rb b/spec/requests/mutations/resend_confirmation_spec.rb new file mode 100644 index 00000000..8dcf5b04 --- /dev/null +++ b/spec/requests/mutations/resend_confirmation_spec.rb @@ -0,0 +1,83 @@ +require 'rails_helper' + +RSpec.describe 'Resend confirmation' do + include_context 'with graphql query request' + + let(:user) { create(:user, confirmed_at: nil) } + let(:email) { user.email } + let(:id) { user.id } + let(:redirect) { Faker::Internet.url } + let(:query) do + <<-GRAPHQL + mutation { + userResendConfirmation( + email:"#{email}", + redirectUrl:"#{redirect}" + ) { + message + authenticable { + id + email + } + } + } + GRAPHQL + end + + context 'when params are correct' do + it 'sends an email to the user with confirmation url and returns a success message' do + expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1) + expect(json_response[:data][:userResendConfirmation]).to include( + authenticable: { + id: id, + email: email + }, + message: "You will receive an email with instructions for how to confirm your email address in a few minutes." + ) + + email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded) + link = email.css('a').first + confirm_link_msg_text = email.css('p')[1].inner_html + confirm_account_link_text = link.inner_html + + expect(confirm_link_msg_text).to eq("You can confirm your account email through the link below:") + expect(confirm_account_link_text).to eq("Confirm my account") + + # TODO: Move to feature spec + expect do + get link['href'] + user.reload + end.to change(user, :confirmed_at).from(NilClass).to(ActiveSupport::TimeWithZone) + end + + context 'when the user has already been confirmed' do + before { user.confirm } + + it 'does *NOT* send an email and raises an error' do + expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count) + expect(json_response[:data][:userResendConfirmation]).to be_nil + expect(json_response[:errors]).to contain_exactly( + hash_including( + message: "Email was already confirmed, please try signing in", + extensions: { code: 'USER_ERROR' } + ) + ) + end + end + end + + context "when the email isn't in the system" do + let(:email) { 'nothere@gmail.com' } + + it 'does *NOT* send an email and raises an error' do + expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count) + expect(json_response[:data][:userResendConfirmation]).to be_nil + expect(json_response[:errors]).to contain_exactly( + hash_including( + message: "Unable to find user with email '#{email}'.", + extensions: { code: 'USER_ERROR' } + ) + ) + end + end +end