diff --git a/Gemfile b/Gemfile index 1a49130..bf015c5 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' gem 'byebug' -gem 'rails', '~> 5.2' +gem 'rails', '~> 6.0' gem 'graphql', '~> 1.9.6' diff --git a/README.md b/README.md index 0590d1e..4fedac1 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,8 @@ GraphQL::Auth.configure do |config| # config.allow_sign_up = true # config.allow_lock_account = false # config.allow_unlock_account = false + # config.allow_email_confirmable = false + # Allow custom mutations for signup and update account # config.sign_up_mutation = '::Mutations::Auth::SignUp' diff --git a/app/graphql/mutations/auth/resend_confirmation_instructions.rb b/app/graphql/mutations/auth/resend_confirmation_instructions.rb new file mode 100644 index 0000000..ccbd304 --- /dev/null +++ b/app/graphql/mutations/auth/resend_confirmation_instructions.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Mutations::Auth::ResendConfirmationInstructions < GraphQL::Schema::Mutation + include ::Graphql::AccountLockHelper + + argument :email, String, required: true do + description 'The email to confirm.' + end + + field :errors, [::Types::Auth::Error], null: false + field :success, Boolean, null: false + field :valid, Boolean, null: false + + def resolve(email:) + if lockable? + user = User.where(locked_at: nil).find_by email: email + else + user = User.find_by email: email + end + + user.send_confirmation_instructions if user.present? + + { + errors: [], + success: true, + valid: true + } + end +end diff --git a/app/graphql/mutations/auth/sign_in.rb b/app/graphql/mutations/auth/sign_in.rb index db13808..6e61f4a 100644 --- a/app/graphql/mutations/auth/sign_in.rb +++ b/app/graphql/mutations/auth/sign_in.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - class Mutations::Auth::SignIn < GraphQL::Schema::Mutation include ::Graphql::AccountLockHelper include ::Graphql::TokenHelper @@ -23,31 +21,63 @@ class Mutations::Auth::SignIn < GraphQL::Schema::Mutation def resolve(email:, password:, remember_me:) response = context[:response] - if lockable? - user = User.where(locked_at: nil).find_by email: email - else - user = User.find_by email: email + user = User.find_by email: email + valid_sign_in = user.present? && user.valid_password?(password) + + device_lockable_enabled = user.lock_strategy_enabled?(:failed_attempts) + + if user.access_locked? + return { + errors: [ + { + field: :_error, + message: I18n.t('devise.failure.locked') + } + ], + success: false, + user: nil + } end - valid_sign_in = user.present? && user.valid_password?(password) + if device_lockable_enabled && !valid_sign_in + user.increment_failed_attempts + + if user.send('attempts_exceeded?') + user.lock_access! unless user.access_locked? + + return { + errors: [ + { + field: :_error, + message: I18n.t('devise.failure.locked') + } + ], + success: false, + user: nil + } + else + user.save(validate: false) + end + end + + # TODO tests && error messages + if valid_sign_in generate_access_token(user, response) set_current_user(user) remember_me ? set_refresh_token(user, response) : delete_refresh_token(user) - { errors: [], success: true, user: user } else - { + return { errors: [ { field: :_error, - message: I18n.t('devise.failure.invalid', - authentication_keys: I18n.t('activerecord.attributes.user.email')) + message: I18n.t('devise.failure.noaccess') } ], success: false, @@ -55,4 +85,4 @@ def resolve(email:, password:, remember_me:) } end end -end +end \ No newline at end of file diff --git a/app/graphql/types/graphql_auth.rb b/app/graphql/types/graphql_auth.rb index 85bea84..b9e436f 100644 --- a/app/graphql/types/graphql_auth.rb +++ b/app/graphql/types/graphql_auth.rb @@ -16,6 +16,10 @@ module Types::GraphqlAuth field :validate_token, mutation: ::Mutations::Auth::ValidateToken + if GraphQL::Auth.configuration.allow_email_confirmable + field :resend_confirmation_instructions, mutation: ::Mutations::Auth::ResendConfirmationInstructions + end + if GraphQL::Auth.configuration.allow_lock_account field :lock_account, mutation: Mutations::Auth::LockAccount end diff --git a/graphql-1.9.gemfile b/graphql-1.9.gemfile index e81403d..538f49b 100644 --- a/graphql-1.9.gemfile +++ b/graphql-1.9.gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' gem 'byebug' gem 'coveralls' -gem 'rails', '~> 5.2' +gem 'rails', '~> 6.0' gem 'graphql', '~> 1.9.6' gemspec \ No newline at end of file diff --git a/graphql-auth.gemspec b/graphql-auth.gemspec index 2941f9e..979b476 100644 --- a/graphql-auth.gemspec +++ b/graphql-auth.gemspec @@ -17,12 +17,12 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.4.5' - spec.add_dependency "rails", "~> 5.1" + spec.add_dependency "rails", "~> 6.0" spec.add_dependency 'graphql', '~> 1.9', '>= 1.9.6' spec.add_dependency 'devise', '~> 4.6', '>= 4.6.2' - spec.add_dependency 'jwt', '~> 1.5' + spec.add_dependency 'jwt', '~> 2.1' - spec.add_development_dependency 'sqlite3', '~> 1.3.6' + spec.add_development_dependency 'sqlite3', '~> 1.4' spec.add_development_dependency 'bundler', '~> 2.0' spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rspec', '~> 3.0' diff --git a/lib/graphql-auth/configuration.rb b/lib/graphql-auth/configuration.rb index 6a8b781..18e3d37 100644 --- a/lib/graphql-auth/configuration.rb +++ b/lib/graphql-auth/configuration.rb @@ -8,6 +8,7 @@ class Configuration :allow_sign_up, :allow_lock_account, :allow_unlock_account, + :allow_email_confirmable, :sign_up_mutation, :update_account_mutation @@ -22,6 +23,7 @@ def initialize @allow_sign_up = true @allow_lock_account = false @allow_unlock_account = false + @allow_email_confirmable = false # Allow custom mutations for signup and update account @sign_up_mutation = '::Mutations::Auth::SignUp' diff --git a/lib/graphql-auth/engine.rb b/lib/graphql-auth/engine.rb index d85710c..6a815c7 100644 --- a/lib/graphql-auth/engine.rb +++ b/lib/graphql-auth/engine.rb @@ -3,7 +3,7 @@ module Auth class Engine < ::Rails::Engine isolate_namespace GraphQL::Auth - config.autoload_paths += Dir["#{config.root}/app/**/"] + config.autoload_paths += Dir["#{config.root}/app/**/*.rb"] end end end \ No newline at end of file diff --git a/lib/graphql-auth/jwt_manager.rb b/lib/graphql-auth/jwt_manager.rb index b2574f7..6a70af1 100644 --- a/lib/graphql-auth/jwt_manager.rb +++ b/lib/graphql-auth/jwt_manager.rb @@ -1,5 +1,4 @@ require 'jwt' -require 'graphql-auth' module GraphQL module Auth diff --git a/lib/graphql-auth/version.rb b/lib/graphql-auth/version.rb index bd1f349..897b874 100644 --- a/lib/graphql-auth/version.rb +++ b/lib/graphql-auth/version.rb @@ -1,5 +1,5 @@ module GraphQL module Auth - VERSION = '0.6.1' + VERSION = '0.7.8' end end \ No newline at end of file diff --git a/spec/dummy/app/assets/config/manifest.js b/spec/dummy/app/assets/config/manifest.js new file mode 100644 index 0000000..e69de29 diff --git a/spec/dummy/app/graphql/graphql_schema.rb b/spec/dummy/app/graphql/graphql_schema.rb index 39d1556..ad9cd2e 100644 --- a/spec/dummy/app/graphql/graphql_schema.rb +++ b/spec/dummy/app/graphql/graphql_schema.rb @@ -2,7 +2,7 @@ class GraphqlSchema < GraphQL::Schema mutation Types::MutationType - query Types::QueryType + # query Types::QueryType end GraphqlSchema.graphql_definition diff --git a/spec/dummy/app/models/user.rb b/spec/dummy/app/models/user.rb index 40a343d..f28f392 100644 --- a/spec/dummy/app/models/user.rb +++ b/spec/dummy/app/models/user.rb @@ -4,7 +4,7 @@ class User < ApplicationRecord extend Devise::Models # Include default devise modules. Others available are: - # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + # :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :lockable, :registerable, - :recoverable, :rememberable, :validatable + :recoverable, :rememberable, :validatable, :confirmable end diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index 2cbcf67..9d00f18 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -8,7 +8,10 @@ module Dummy class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.2 + config.load_defaults 6.0 + + config.autoloader = :classic + # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/spec/dummy/config/environments/development.rb b/spec/dummy/config/environments/development.rb index 366e75a..84b3ccf 100644 --- a/spec/dummy/config/environments/development.rb +++ b/spec/dummy/config/environments/development.rb @@ -58,4 +58,6 @@ # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + config.action_mailer.default_url_options = { host: '0.0.0.0:3000' } + end diff --git a/spec/dummy/config/environments/test.rb b/spec/dummy/config/environments/test.rb index 0a38fd3..5c48b98 100644 --- a/spec/dummy/config/environments/test.rb +++ b/spec/dummy/config/environments/test.rb @@ -43,4 +43,6 @@ # Raises error for missing translations # config.action_view.raise_on_missing_translations = true + + config.action_mailer.default_url_options = { host: '0.0.0.0:3000' } end diff --git a/spec/dummy/config/initializers/graphql_auth.rb b/spec/dummy/config/initializers/graphql_auth.rb index 40920c5..9bf928b 100644 --- a/spec/dummy/config/initializers/graphql_auth.rb +++ b/spec/dummy/config/initializers/graphql_auth.rb @@ -10,6 +10,7 @@ config.allow_sign_up = true config.allow_lock_account = true config.allow_unlock_account = true + config.allow_email_confirmable = true # Allow custom mutations for signup and update account # config.sign_up_mutation = '::Mutations::Auth::SignUp' diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 79de989..768df5d 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -1,5 +1,5 @@ Rails.application.routes.draw do post '/graphql', to: 'graphql#execute' - devise_for :users, skip: :all + devise_for :users#, skip: :all end \ No newline at end of file diff --git a/spec/dummy/db/migrate/20190108110416_devise_create_users.rb b/spec/dummy/db/migrate/20190108110416_devise_create_users.rb index e4fe133..6004021 100644 --- a/spec/dummy/db/migrate/20190108110416_devise_create_users.rb +++ b/spec/dummy/db/migrate/20190108110416_devise_create_users.rb @@ -22,10 +22,10 @@ def change # t.inet :last_sign_in_ip ## Confirmable - # t.string :confirmation_token - # t.datetime :confirmed_at - # t.datetime :confirmation_sent_at - # t.string :unconfirmed_email # Only if using reconfirmable + t.string :confirmation_token + t.datetime :confirmed_at + t.datetime :confirmation_sent_at + t.string :unconfirmed_email # Only if using reconfirmable ## Lockable # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index 88ca44d..a7dff7e 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -2,11 +2,11 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. @@ -39,6 +39,10 @@ t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" + t.string "confirmation_token" + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "refresh_token" @@ -47,4 +51,5 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" end diff --git a/spec/graphql/mutations/auth/forgot_password_spec.rb b/spec/graphql/mutations/auth/forgot_password_spec.rb index 0b35bdc..985d121 100644 --- a/spec/graphql/mutations/auth/forgot_password_spec.rb +++ b/spec/graphql/mutations/auth/forgot_password_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Mutations::Auth::ForgotPassword, type: :request do - let!(:user) { User.create!(email: 'user@example.com', password: 'password') } + let!(:user) { User.create!(email: 'user@example.com', password: 'password', confirmed_at: DateTime.now) } let(:result) do GraphqlSchema.execute( @@ -87,7 +87,7 @@ end context 'when user is locked' do - let!(:locked_user) { User.create!(email: 'locked_user@example.com', password: 'password') } + let!(:locked_user) { User.create!(email: 'locked_user@example.com', password: 'password', confirmed_at: DateTime.now) } let(:variables) do { diff --git a/spec/graphql/mutations/auth/resend_confirmation_instructions_spec.rb b/spec/graphql/mutations/auth/resend_confirmation_instructions_spec.rb new file mode 100644 index 0000000..d0b735d --- /dev/null +++ b/spec/graphql/mutations/auth/resend_confirmation_instructions_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Auth::ResendConfirmationInstructions, type: :request do + let!(:user) { User.create!(email: 'unconfirmed_user@example.com', password: 'password') } + + let(:result) do + GraphqlSchema.execute( + query_string, + variables: variables, + context: context + ) + end + + let(:query_string) do + <<-GRAPHQL + mutation($email: String!) { + resendConfirmationInstructions(email: $email) { + errors { + field + message + } + success + valid + } + } + GRAPHQL + end + + let(:context) do + { + current_user: nil, + response: ResponseMock.new(headers: {}), + } + end + + subject { result } + + context 'when valid parameters are given' do + let(:variables) do + { + 'email' => user.email + } + end + + before do + subject + end + + it 'sends a email confirmation instructions email' do + email = ActionMailer::Base.deliveries.find{|email| email[:To].value == variables['email'] } + + expect(email[:To].value).to eq(user.email) + expect(email[:Subject].value).to eq('Confirmation instructions') + end + + it 'returns a success' do + expect(result['data']['resendConfirmationInstructions']['errors']).to match_array([]) + expect(result['data']['resendConfirmationInstructions']['success']).to be_truthy + expect(result['data']['resendConfirmationInstructions']['valid']).to be_truthy + end + end + + context 'when invalid parameters are given' do + let(:variables) do + { + 'email' => 'bademail@example.com' + } + end + + before do + subject + end + + it 'does not sends a email confirmation instructions email' do + email = ActionMailer::Base.deliveries.find{|email| email[:To].value == variables['email'] } + expect(email).to be_nil + end + + it 'gives no clue about the failure' do + subject + expect(result['data']['resendConfirmationInstructions']['errors']).to match_array([]) + expect(result['data']['resendConfirmationInstructions']['success']).to be_truthy + expect(result['data']['resendConfirmationInstructions']['valid']).to be_truthy + end + end + + context 'when user is locked' do + let!(:locked_user) { User.create!(email: 'unconfirmed_locked_user@example.com', password: 'password') } + + let(:variables) do + { + 'email' => locked_user.email + } + end + + before do + locked_user.lock_access! + subject + end + + it 'does not sends a email confirmation instructions email' do + emails = ActionMailer::Base.deliveries.find_all{|email| email[:To].value == variables['email'] } + expect(emails.count).to eq(1) # because when created the user he already got one mail + end + + it 'gives no clue about the failure' do + subject + expect(result['data']['resendConfirmationInstructions']['errors']).to match_array([]) + expect(result['data']['resendConfirmationInstructions']['success']).to be_truthy + expect(result['data']['resendConfirmationInstructions']['valid']).to be_truthy + end + end +end diff --git a/spec/graphql/mutations/auth/sign_in_spec.rb b/spec/graphql/mutations/auth/sign_in_spec.rb index c9a3fd7..80fed5d 100644 --- a/spec/graphql/mutations/auth/sign_in_spec.rb +++ b/spec/graphql/mutations/auth/sign_in_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Mutations::Auth::SignUp, type: :request do let!(:user) do - User.create!(email: 'email@example.com', password: 'password') + User.create!(email: 'email@example.com', password: 'password', confirmed_at: DateTime.now) end let(:result) do