diff --git a/.gitignore b/.gitignore index 4200a43f..9399d2f4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ /*.sqlite3 /spec/dummy/db/development.sqlite3 /spec/dummy/db/test.sqlite3 +/*.gem diff --git a/app/graphql/graphql_devise/mutations/base.rb b/app/graphql/graphql_devise/mutations/base.rb index 20e3a40f..25457299 100644 --- a/app/graphql/graphql_devise/mutations/base.rb +++ b/app/graphql/graphql_devise/mutations/base.rb @@ -50,6 +50,11 @@ def client controller.token.client if controller.token.present? end end + + def set_auth_headers(resource) + auth_headers = resource.create_new_auth_token + response.headers.merge!(auth_headers) + end end end end diff --git a/app/graphql/graphql_devise/mutations/login.rb b/app/graphql/graphql_devise/mutations/login.rb index 9c5ba937..b6ba5f23 100644 --- a/app/graphql/graphql_devise/mutations/login.rb +++ b/app/graphql/graphql_devise/mutations/login.rb @@ -31,11 +31,6 @@ def resolve(email:, password:) private - def set_auth_headers(resource) - auth_headers = resource.create_new_auth_token - response.headers.merge!(auth_headers) - end - def invalid_for_authentication?(resource, password) valid_password = resource.valid_password?(password) diff --git a/app/graphql/graphql_devise/mutations/sign_up.rb b/app/graphql/graphql_devise/mutations/sign_up.rb new file mode 100644 index 00000000..ee5047cf --- /dev/null +++ b/app/graphql/graphql_devise/mutations/sign_up.rb @@ -0,0 +1,61 @@ +module GraphqlDevise + module Mutations + class SignUp < Base + argument :email, String, required: true + argument :password, String, required: true + argument :password_confirmation, String, required: true + argument :confirm_success_url, String, required: false + argument :config_name, String, required: false + + def resolve(confirm_success_url: nil, config_name: nil, **attrs) + resource = resource_class.new(provider: provider, **attrs) + + if resource.present? + resource.skip_confirmation_notification! if resource.respond_to?(:skip_confirmation_notification!) + + if resource.save + yield resource if block_given? + + if confirmable_enabled?(resource) && !resource.confirmed? + # user will require email authentication + resource.send_confirmation_instructions( + client_config: config_name, + redirect_url: confirm_success_url + ) + end + + set_auth_headers(resource) if active_for_authentication?(resource) + + { authenticable: resource } + else + clean_up_passwords(resource) + raise_user_error_list( + I18n.t('graphql_devise.registration_failed'), + errors: resource.errors.full_messages + ) + end + else + raise_user_error(I18n.t('graphql_devise.resource_build_failed')) + end + end + + protected + + def confirmable_enabled?(resource) + resource.respond_to?(:confirmed_at) + end + + def active_for_authentication?(resource) + resource.active_for_authentication? + end + + def provider + :email + end + + def clean_up_passwords(resource) + controller.send(:clean_up_passwords, resource) + end + end + end +end diff --git a/app/helpers/graphql_devise/mailer_helper.rb b/app/helpers/graphql_devise/mailer_helper.rb new file mode 100644 index 00000000..c7e18fee --- /dev/null +++ b/app/helpers/graphql_devise/mailer_helper.rb @@ -0,0 +1,15 @@ +module GraphqlDevise + module MailerHelper + def confirmation_query(token:, config:, redirect_url:) + raw = <<-GRAPHQL + confirmAccount($token:ID!,$clientConfig:String,redirect:String!){ + userConfirmAccount(token:$token,clientConfig:$clientConfig,redirect:$redirect + ){ + success,errors + } + }&variables={token:"#{token}",clientConfig:"#{config}",redirect:"#{redirect_url}"} + GRAPHQL + ERB::Util.url_encode(raw.gsub("\n", '').gsub(' ', '')) + end + end +end diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 00000000..15624285 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

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

+ +

<%= t '.confirm_link_msg' %>

+ +

<%= link_to t('.confirm_account_link'), api_v1_graphql_auth_url, query: confirmation_query(token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url']).html_safe %>

diff --git a/config/locales/en.yml b/config/locales/en.yml index f1f08252..e0feaa16 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,7 @@ en: graphql_devise: + registration_failed: "User couldn't be registered" + resource_build_failed: "Resource couldn't be built, execution stopped." not_authenticated: "User is not logged in." user_not_found: "User was not found or was not logged in." passwords: diff --git a/graphql_devise.gemspec b/graphql_devise.gemspec index e62698e6..de57779b 100644 --- a/graphql_devise.gemspec +++ b/graphql_devise.gemspec @@ -33,6 +33,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'factory_bot' spec.add_development_dependency 'faker' spec.add_development_dependency 'pry' + spec.add_development_dependency 'pry-byebug' spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rspec-rails' spec.add_development_dependency 'rubocop', '0.68.1' diff --git a/lib/graphql_devise.rb b/lib/graphql_devise.rb index b747da92..aa38c9c4 100644 --- a/lib/graphql_devise.rb +++ b/lib/graphql_devise.rb @@ -1,7 +1,7 @@ require 'rails' -require 'graphql_devise/engine' -require 'devise_token_auth' require 'graphql' +require 'devise_token_auth' +require 'graphql_devise/engine' require 'graphql_devise/version' require 'graphql_devise/error_codes' require 'graphql_devise/user_error' diff --git a/lib/graphql_devise/rails/routes.rb b/lib/graphql_devise/rails/routes.rb index 250306df..4ea60070 100644 --- a/lib/graphql_devise/rails/routes.rb +++ b/lib/graphql_devise/rails/routes.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - module ActionDispatch::Routing class Mapper def mount_graphql_devise_for(resource, opts = {}) @@ -21,6 +19,7 @@ def mount_graphql_devise_for(resource, opts = {}) default_mutations = { login: GraphqlDevise::Mutations::Login, logout: GraphqlDevise::Mutations::Logout, + sign_up: GraphqlDevise::Mutations::SignUp, update_password: GraphqlDevise::Mutations::UpdatePassword }.freeze @@ -38,6 +37,8 @@ def mount_graphql_devise_for(resource, opts = {}) GraphqlDevise::Types::MutationType.field("#{mapping_name}_#{action}", mutation: used_mutation) end + Devise.mailer.send(:add_template_helper, GraphqlDevise::MailerHelper) + devise_scope mapping_name.to_sym do post "#{path}/graphql_auth", to: 'graphql_devise/graphql#auth' end diff --git a/spec/dummy/app/graphql/mutations/sign_up.rb b/spec/dummy/app/graphql/mutations/sign_up.rb new file mode 100644 index 00000000..4c2220f4 --- /dev/null +++ b/spec/dummy/app/graphql/mutations/sign_up.rb @@ -0,0 +1,12 @@ +module Mutations + class SignUp < GraphqlDevise::Mutations::SignUp + argument :name, String, required: false + + field :user, Types::UserType, null: true + + def resolve(email:, **attrs) + original_payload = super + original_payload.merge(user: original_payload[:authenticable]) + end + end +end diff --git a/spec/dummy/config/environments/test.rb b/spec/dummy/config/environments/test.rb index d1bbf069..6f4f37a4 100644 --- a/spec/dummy/config/environments/test.rb +++ b/spec/dummy/config/environments/test.rb @@ -35,6 +35,7 @@ # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 5c6345db..f6323325 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -1,5 +1,6 @@ Rails.application.routes.draw do mount_graphql_devise_for 'User', at: 'api/v1', mutations: { - login: Mutations::Login + login: Mutations::Login, + sign_up: Mutations::SignUp } end diff --git a/spec/requests/mutations/sign_up_spec.rb b/spec/requests/mutations/sign_up_spec.rb new file mode 100644 index 00000000..fd34b2a9 --- /dev/null +++ b/spec/requests/mutations/sign_up_spec.rb @@ -0,0 +1,71 @@ +require 'rails_helper' + +RSpec.describe '' do + include_context 'with graphql query request' + + let(:name) { Faker::Name.name } + let(:password) { Faker::Internet.password } + let(:email) { Faker::Internet.email } + let(:redirect) { Faker::Internet.url } + let(:query) do + <<-GRAPHQL + mutation { + userSignUp( + email: "#{email}" + name: "#{name}" + password: "#{password}" + passwordConfirmation: "#{password}" + confirmSuccessUrl: "#{redirect}" + ) { + user { + email + name + } + } + } + GRAPHQL + end + + context 'when params are correct' do + it 'creates a new resource that requires confirmation' do + expect { post_request }.to( + change(User, :count).by(1) + .and(change(ActionMailer::Base.deliveries, :count).by(1)) + ) + + user = User.last + expect(user).not_to be_active_for_authentication + expect(user.confirmed_at).to be_nil + expect(user.valid_password?(password)).to be_truthy + expect(json_response[:data][:userSignUp]).to include( + user: { + email: email, + name: name + } + ) + + email = ActionMailer::Base.deliveries.last + query = ERB::Util.url_encode("confirmAccount($token:ID!,$clientConfig:String,redirect:String!){userConfirmAccount(token:$token,clientConfig:$clientConfig,redirect:$redirect){success,errors}}&variables={token:\"#{user.confirmation_token}\",clientConfig:\"default\",redirect:\"#{redirect}\"}").html_safe + expect(email.body.encoded).to match(/query="#{query}"/) + end + end + + context 'when required params are missing' do + let(:email) { '' } + + it 'does *NOT* create resource a resource nor send an email' do + expect { post_request }.to( + not_change(User, :count) + .and(not_change(ActionMailer::Base.deliveries, :count)) + ) + + expect(json_response[:data][:userSignUp]).to be_nil + expect(json_response[:errors]).to containing_exactly( + hash_including( + message: "User couldn't be registered", + extensions: { code: 'USER_ERROR', detailed_errors: ["Email can't be blank"] } + ) + ) + end + end +end diff --git a/spec/support/matchers/not_change_matcher.rb b/spec/support/matchers/not_change_matcher.rb new file mode 100644 index 00000000..93f5b66e --- /dev/null +++ b/spec/support/matchers/not_change_matcher.rb @@ -0,0 +1 @@ +RSpec::Matchers.define_negated_matcher :not_change, :change