├── app ├── views │ ├── .keep │ └── graphql_devise │ │ └── mailer │ │ ├── confirmation_instructions.html.erb │ │ └── reset_password_instructions.html.erb ├── helpers │ └── graphql_devise │ │ └── application_helper.rb └── controllers │ └── graphql_devise │ ├── application_controller.rb │ └── graphql_controller.rb ├── spec ├── dummy │ ├── app │ │ ├── assets │ │ │ └── config │ │ │ │ └── manifest.js │ │ ├── views │ │ │ └── layouts │ │ │ │ ├── mailer.text.erb │ │ │ │ └── mailer.html.erb │ │ ├── jobs │ │ │ └── application_job.rb │ │ ├── models │ │ │ ├── users.rb │ │ │ ├── application_record.rb │ │ │ ├── guest.rb │ │ │ ├── users │ │ │ │ └── customer.rb │ │ │ ├── admin.rb │ │ │ ├── schema_user.rb │ │ │ └── user.rb │ │ ├── graphql │ │ │ ├── types │ │ │ │ ├── base_object.rb │ │ │ │ ├── admin_type.rb │ │ │ │ ├── custom_admin_type.rb │ │ │ │ ├── user_type.rb │ │ │ │ ├── mutation_type.rb │ │ │ │ └── query_type.rb │ │ │ ├── mutations │ │ │ │ ├── base_mutation.rb │ │ │ │ ├── register.rb │ │ │ │ ├── reset_admin_password_with_token.rb │ │ │ │ ├── login.rb │ │ │ │ ├── update_user.rb │ │ │ │ └── register_confirmed_user.rb │ │ │ ├── resolvers │ │ │ │ ├── user_show.rb │ │ │ │ └── public_user.rb │ │ │ ├── interpreter_schema.rb │ │ │ └── dummy_schema.rb │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ ├── cookies_controller.rb │ │ │ └── api │ │ │ │ └── v1 │ │ │ │ └── graphql_controller.rb │ │ └── mailers │ │ │ └── application_mailer.rb │ ├── config │ │ ├── master.key │ │ ├── initializers │ │ │ ├── i18n.rb │ │ │ ├── mime_types.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── application_controller_renderer.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── wrap_parameters.rb │ │ │ ├── cors.rb │ │ │ ├── inflections.rb │ │ │ └── devise_token_auth.rb │ │ ├── spring.rb │ │ ├── environment.rb │ │ ├── secrets.yml │ │ ├── database.yml │ │ ├── credentials.yml.enc │ │ ├── application.rb │ │ ├── routes.rb │ │ ├── puma.rb │ │ └── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ ├── bin │ │ ├── rake │ │ ├── rails │ │ ├── bundle │ │ ├── update │ │ └── setup │ ├── public │ │ └── robots.txt │ ├── config.ru │ ├── db │ │ ├── migrate │ │ │ ├── 20210516211417_add_vip_to_users.rb │ │ │ ├── 20200621182414_remove_uncofirmed_email_from_admins.rb │ │ │ ├── 20190824215150_add_auth_available_to_users.rb │ │ │ ├── 20200321121807_create_users_customers.rb │ │ │ ├── 20190916012505_create_admins.rb │ │ │ ├── 20191013213045_create_guests.rb │ │ │ ├── 20200623003142_create_schema_users.rb │ │ │ └── 20190815114303_create_users.rb │ │ └── seeds.rb │ ├── Rakefile │ └── README.md ├── support │ ├── matchers │ │ ├── not_change_matcher.rb │ │ └── auth_headers_matcher.rb │ ├── requests │ │ ├── auth_helpers.rb │ │ └── json_helpers.rb │ ├── factory_bot.rb │ └── contexts │ │ ├── schema_test.rb │ │ └── graphql_request.rb ├── graphql_devise_spec.rb ├── models │ └── user_spec.rb ├── factories │ ├── admins.rb │ ├── guests.rb │ ├── users_customers.rb │ ├── schema_users.rb │ └── users.rb ├── services │ ├── mount_method │ │ ├── operation_preparers │ │ │ ├── resource_klass_setter_spec.rb │ │ │ ├── gql_name_setter_spec.rb │ │ │ ├── resolver_type_setter_spec.rb │ │ │ ├── mutation_field_setter_spec.rb │ │ │ ├── custom_operation_preparer_spec.rb │ │ │ └── default_operation_preparer_spec.rb │ │ ├── option_sanitizers │ │ │ ├── string_checker_spec.rb │ │ │ ├── array_checker_spec.rb │ │ │ ├── class_checker_spec.rb │ │ │ └── hash_checker_spec.rb │ │ ├── operation_sanitizer_spec.rb │ │ ├── option_validators │ │ │ ├── skip_only_validator_spec.rb │ │ │ ├── supported_operations_validator_spec.rb │ │ │ └── provided_operations_validator_spec.rb │ │ ├── operation_preparer_spec.rb │ │ ├── options_validator_spec.rb │ │ └── option_sanitizer_spec.rb │ └── schema_plugin_spec.rb ├── spec_helper.rb ├── requests │ ├── mutations │ │ ├── additional_queries_spec.rb │ │ ├── logout_spec.rb │ │ ├── additional_mutations_spec.rb │ │ ├── send_password_reset_with_token_spec.rb │ │ ├── confirm_registration_with_token_spec.rb │ │ └── update_password_with_token_spec.rb │ └── graphql_controller_spec.rb ├── rails_helper.rb ├── graphql │ └── user_queries_spec.rb └── generators │ └── graphql_devise │ └── install_generator_spec.rb ├── .rspec ├── gemfiles ├── .bundle │ └── config ├── rails7.2_graphql2.5.gemfile ├── rails8.0_graphql2.2.gemfile ├── rails8.0_graphql2.3.gemfile ├── rails8.0_graphql2.4.gemfile ├── rails8.0_graphql2.5.gemfile ├── rails6.1_graphql1.10.gemfile ├── rails6.1_graphql1.13.gemfile ├── rails7.2_graphql2.0.gemfile ├── rails7.2_graphql2.1.gemfile ├── rails7.2_graphql2.2.gemfile ├── rails7.2_graphql2.3.gemfile ├── rails7.2_graphql2.4.gemfile ├── rails6.1_graphql1.11.gemfile ├── rails6.1_graphql1.12.gemfile ├── rails6.1_graphql2.0.gemfile ├── rails7.0_graphql2.0.gemfile ├── rails7.0_graphql2.1.gemfile ├── rails7.0_graphql2.2.gemfile ├── rails7.0_graphql2.3.gemfile ├── rails7.0_graphql2.4.gemfile ├── rails7.1_graphql2.0.gemfile ├── rails7.1_graphql2.1.gemfile ├── rails7.1_graphql2.2.gemfile ├── rails7.1_graphql2.3.gemfile └── rails7.1_graphql2.4.gemfile ├── Gemfile ├── lib ├── graphql_devise │ ├── version.rb │ ├── schema.rb │ ├── errors │ │ ├── execution_error.rb │ │ ├── error_codes.rb │ │ ├── user_error.rb │ │ ├── authentication_error.rb │ │ └── detailed_user_error.rb │ ├── engine.rb │ ├── types │ │ ├── base_type.rb │ │ ├── base_field.rb │ │ ├── authenticatable_type.rb │ │ ├── query_type.rb │ │ ├── mutation_type.rb │ │ └── credential_type.rb │ ├── mutations │ │ ├── base.rb │ │ ├── logout.rb │ │ ├── confirm_registration_with_token.rb │ │ ├── send_password_reset_with_token.rb │ │ ├── resend_confirmation_with_token.rb │ │ ├── update_password_with_token.rb │ │ ├── login.rb │ │ └── register.rb │ ├── resolvers │ │ ├── base.rb │ │ └── dummy.rb │ ├── concerns │ │ ├── set_user_by_token.rb │ │ ├── authenticatable.rb │ │ ├── field_authentication.rb │ │ ├── additional_model_methods.rb │ │ ├── auth_controller_methods.rb │ │ ├── additional_controller_methods.rb │ │ └── controller_methods.rb │ ├── mount_method │ │ ├── options_validator.rb │ │ ├── operation_preparers │ │ │ ├── resource_klass_setter.rb │ │ │ ├── resolver_type_setter.rb │ │ │ ├── gql_name_setter.rb │ │ │ ├── mutation_field_setter.rb │ │ │ ├── custom_operation_preparer.rb │ │ │ └── default_operation_preparer.rb │ │ ├── option_sanitizer.rb │ │ ├── option_validators │ │ │ ├── skip_only_validator.rb │ │ │ ├── supported_operations_validator.rb │ │ │ └── provided_operations_validator.rb │ │ ├── option_sanitizers │ │ │ ├── string_checker.rb │ │ │ ├── hash_checker.rb │ │ │ ├── array_checker.rb │ │ │ └── class_checker.rb │ │ ├── operation_sanitizer.rb │ │ ├── supported_options.rb │ │ └── operation_preparer.rb │ ├── route_mounter.rb │ ├── default_operations.rb │ ├── field_auth_tracer.rb │ ├── model │ │ └── with_email_updater.rb │ └── schema_plugin.rb ├── graphql_devise.rb └── generators │ └── graphql_devise │ └── install_generator.rb ├── bin ├── setup ├── install_bundler.rb ├── console └── rails ├── .github └── ISSUE_TEMPLATE │ ├── question.md │ ├── enhancement.md │ └── bug_report.md ├── .gitignore ├── config ├── routes.rb └── locales │ ├── en.yml │ └── pt-BR.yml ├── Rakefile ├── LICENSE.txt ├── graphql_devise.gemspec ├── .rubocop.yml ├── docs └── usage │ └── reset_password_flow.md └── .circleci └── config.yml /app/views/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /spec/dummy/config/master.key: -------------------------------------------------------------------------------- 1 | 46804cd14fb86423326fd402c4b455cf -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --order random 4 | -------------------------------------------------------------------------------- /gemfiles/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_RETRY: "1" 3 | BUNDLE_WITHOUT: "production" 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rake' 3 | Rake.application.run 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /lib/graphql_devise/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | VERSION = '2.1.0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationJob < ActiveJob::Base 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require 'rails/commands' 4 | -------------------------------------------------------------------------------- /lib/graphql_devise/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | class Schema < GraphQL::Schema 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/matchers/not_change_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Matchers.define_negated_matcher :not_change, :change 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | def self.table_name_prefix 5 | 'users_' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/helpers/graphql_devise/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module ApplicationHelper 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/types/base_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseObject < GraphQL::Schema::Object 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /spec/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /lib/graphql_devise/errors/execution_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | class ExecutionError < GraphQL::ExecutionError 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/mutations/base_mutation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class BaseMutation < GraphQL::Schema::Mutation 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/i18n.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | I18n.exception_handler = lambda do |exception, locale, key, options| 4 | raise exception 5 | end 6 | -------------------------------------------------------------------------------- /lib/graphql_devise/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | class Engine < ::Rails::Engine 5 | isolate_namespace GraphqlDevise 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/graphql_devise/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | ApplicationController = Class.new(ActionController::API) 5 | end 6 | -------------------------------------------------------------------------------- /lib/graphql_devise/types/base_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Types 5 | class BaseType < GraphQL::Schema::Object 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationMailer < ActionMailer::Base 4 | default from: 'from@example.com' 5 | layout 'mailer' 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/config/spring.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | %w[ 4 | .ruby-version 5 | .rbenv-vars 6 | tmp/restart.txt 7 | tmp/caching-dev.txt 8 | ].each { |path| Spring.watch(path) } 9 | -------------------------------------------------------------------------------- /spec/graphql_devise_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe GraphqlDevise do 4 | it 'has a version number' do 5 | expect(GraphqlDevise::VERSION).not_to be_nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative 'config/environment' 6 | 7 | run Rails.application 8 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20210516211417_add_vip_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddVipToUsers < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :users, :vip, :boolean, null: false, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative 'application' 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /spec/support/requests/auth_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Requests 4 | module AuthHelpers 5 | def auth_headers_for(user) 6 | user.create_new_auth_token 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/types/admin_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class AdminType < BaseObject 5 | field :id, Int, null: false 6 | field :email, String, null: false 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/cookies_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CookiesController < ApplicationController 4 | include ActionController::Cookies 5 | protect_from_forgery with: :null_session 6 | end 7 | 8 | -------------------------------------------------------------------------------- /lib/graphql_devise/errors/error_codes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | ERROR_CODES = { 5 | user_error: 'USER_ERROR', 6 | authentication_error: 'AUTHENTICATION_ERROR' 7 | }.freeze 8 | end 9 | -------------------------------------------------------------------------------- /lib/graphql_devise/types/base_field.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Types 5 | class BaseField < GraphQL::Schema::Field 6 | include FieldAuthentication 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.include FactoryBot::Syntax::Methods 5 | 6 | config.before(:suite) do 7 | FactoryBot.find_definitions 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new mime types for use in respond_to blocks: 6 | # Mime::Type.register "text/richtext", :rtf 7 | -------------------------------------------------------------------------------- /lib/graphql_devise/types/authenticatable_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Types 5 | class AuthenticatableType < BaseType 6 | field :email, String, null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /gemfiles/rails7.2_graphql2.5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", git: "https://github.com/rails/rails", branch: "7-2-stable" 6 | gem "graphql", ">= 2.5", "< 2.6" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails8.0_graphql2.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", git: "https://github.com/rails/rails", branch: "8-0-stable" 6 | gem "graphql", ">= 2.2", "< 2.3" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails8.0_graphql2.3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", git: "https://github.com/rails/rails", branch: "8-0-stable" 6 | gem "graphql", ">= 2.3", "< 2.4" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails8.0_graphql2.4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", git: "https://github.com/rails/rails", branch: "8-0-stable" 6 | gem "graphql", ">= 2.4", "< 2.5" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails8.0_graphql2.5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", git: "https://github.com/rails/rails", branch: "8-0-stable" 6 | gem "graphql", ">= 2.5", "< 2.6" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /lib/graphql_devise/errors/user_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | class UserError < ExecutionError 5 | def to_h 6 | super.merge(extensions: { code: ERROR_CODES.fetch(:user_error) }) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200621182414_remove_uncofirmed_email_from_admins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveUncofirmedEmailFromAdmins < ActiveRecord::Migration[6.0] 4 | def change 5 | remove_column :admins, :unconfirmed_email, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe User do 6 | it 'responds to included concern method' do 7 | user = described_class.new 8 | 9 | expect(user).not_to be_persisted 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20190824215150_add_auth_available_to_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAuthAvailableToUsers < ActiveRecord::Migration[5.2] 4 | def change 5 | add_column :users, :auth_available, :boolean, null: false, default: true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/graphql_devise/mutations/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'devise_token_auth/version' 4 | 5 | module GraphqlDevise 6 | module Mutations 7 | class Base < GraphQL::Schema::Mutation 8 | include ControllerMethods 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/graphql_devise/resolvers/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'devise_token_auth/version' 4 | 5 | module GraphqlDevise 6 | module Resolvers 7 | class Base < GraphQL::Schema::Resolver 8 | include ControllerMethods 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | common_env: &common 2 | secret_key_base: 'secret_key_here' 3 | 4 | development: 5 | <<: *common 6 | 7 | test: 8 | <<: *common 9 | 10 | dev: 11 | <<: *common 12 | 13 | staging: 14 | <<: *common 15 | 16 | production: 17 | <<: *common 18 | -------------------------------------------------------------------------------- /spec/dummy/app/models/guest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Guest < ApplicationRecord 4 | devise :database_authenticatable, 5 | :registerable, 6 | :recoverable, 7 | :validatable 8 | 9 | include GraphqlDevise::Authenticatable 10 | end 11 | -------------------------------------------------------------------------------- /lib/graphql_devise/errors/authentication_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | class AuthenticationError < ExecutionError 5 | def to_h 6 | super.merge(extensions: { code: ERROR_CODES.fetch(:authentication_error) }) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure sensitive parameters which will be filtered from the log file. 6 | Rails.application.config.filter_parameters += [:password] 7 | -------------------------------------------------------------------------------- /spec/support/matchers/auth_headers_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Matchers.define :include_auth_headers do 4 | match do |response| 5 | auth_headers = %w[uid access-token client].map { |key| response.headers[key] } 6 | auth_headers.all?(&:present?) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require_relative 'config/application' 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: sqlite3 3 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 4 | timeout: 5000 5 | 6 | development: 7 | <<: *default 8 | database: db/development.sqlite3 9 | 10 | test: 11 | <<: *default 12 | database: db/test.sqlite3 13 | -------------------------------------------------------------------------------- /app/views/graphql_devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |
<%= t(:welcome).capitalize + ' ' + @email %>!
2 | 3 |<%= t('.confirm_link_msg') %>
4 | 5 |6 | <%= link_to t('.confirm_account_link'), "#{message['redirect-url'].to_s}?#{{ confirmationToken: @token }.to_query}" %> 7 |
8 | -------------------------------------------------------------------------------- /spec/dummy/app/models/users/customer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | class Customer < ApplicationRecord 5 | devise :database_authenticatable, :validatable 6 | 7 | include GraphqlDevise::Authenticatable 8 | 9 | validates :name, presence: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/app/models/admin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Admin < ApplicationRecord 4 | devise :database_authenticatable, 5 | :registerable, 6 | :recoverable, 7 | :validatable, 8 | :confirmable 9 | 10 | include GraphqlDevise::Authenticatable 11 | end 12 | -------------------------------------------------------------------------------- /spec/factories/admins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :admin do 5 | email { Faker::Internet.unique.email } 6 | password { Faker::Internet.password } 7 | 8 | trait :confirmed do 9 | confirmed_at { Time.now } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/factories/guests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :guest do 5 | email { Faker::Internet.unique.email } 6 | password { Faker::Internet.password } 7 | 8 | trait :confirmed do 9 | confirmed_at { Time.now } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/graphql_devise/types/query_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Types 5 | class QueryType < GraphQL::Schema::Object 6 | field_class GraphqlDevise::Types::BaseField if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('2.0') 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/graphql_devise/graphql_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_dependency 'graphql_devise/application_controller' 4 | 5 | module GraphqlDevise 6 | class GraphqlController < ApplicationController 7 | include SetUserByToken 8 | include AuthControllerMethods 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/graphql_devise/types/mutation_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Types 5 | class MutationType < GraphQL::Schema::Object 6 | field_class GraphqlDevise::Types::BaseField if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('2.0') 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/factories/users_customers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :users_customer, class: 'Users::Customer' do 5 | name { Faker::FunnyName.two_word_name } 6 | email { Faker::Internet.unique.email } 7 | password { Faker::Internet.password } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/resolvers/user_show.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Resolvers 4 | class UserShow < GraphQL::Schema::Resolver 5 | type Types::UserType, null: false 6 | 7 | argument :id, Int, required: true 8 | 9 | def resolve(id:) 10 | User.find(id) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/resolvers/public_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Resolvers 4 | class PublicUser < GraphQL::Schema::Resolver 5 | type Types::UserType, null: false 6 | 7 | argument :id, Int, required: true 8 | 9 | def resolve(id:) 10 | User.find(id) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/graphql_devise/concerns/set_user_by_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module SetUserByToken 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | include DeviseTokenAuth::Concerns::SetUserByToken 9 | include AdditionalControllerMethods 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/types/custom_admin_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class CustomAdminType < BaseObject 5 | field :email, String, null: false 6 | field :custom_field, String, null: false 7 | 8 | def custom_field 9 | "email: #{object.email}" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Inquiries about the gem 4 | labels: 'issue: question' 5 | --- 6 | 9 | 10 | ### Question 11 | 12 | 15 | 16 | (Write your question here.) 17 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/types/user_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class UserType < BaseObject 5 | field :id, Int, null: false 6 | field :email, String, null: false 7 | field :name, String, null: false 8 | field :sign_in_count, Int, null: true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # ActiveSupport::Reloader.to_prepare do 6 | # ApplicationController.renderer.defaults.merge!( 7 | # http_host: 'example.org', 8 | # https: false 9 | # ) 10 | # end 11 | -------------------------------------------------------------------------------- /gemfiles/rails6.1_graphql1.10.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sqlite3", "~> 1.5.4" 6 | gem "devise", ">= 4.7" 7 | gem "rails", git: "https://github.com/rails/rails", branch: "6-1-stable" 8 | gem "graphql", "~> 1.10.0" 9 | gem "factory_bot", "<= 6.4.4" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails6.1_graphql1.13.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sqlite3", "~> 1.5.4" 6 | gem "devise", ">= 4.7" 7 | gem "rails", git: "https://github.com/rails/rails", branch: "6-1-stable" 8 | gem "graphql", "~> 1.13.0" 9 | gem "factory_bot", "<= 6.4.4" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails7.2_graphql2.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "devise_token_auth", ">= 1.2.1" 7 | gem "devise", ">= 4.7" 8 | gem "rails", git: "https://github.com/rails/rails", branch: "7-2-stable" 9 | gem "graphql", ">= 2.0", "< 2.1" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails7.2_graphql2.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "devise_token_auth", ">= 1.2.1" 7 | gem "devise", ">= 4.7" 8 | gem "rails", git: "https://github.com/rails/rails", branch: "7-2-stable" 9 | gem "graphql", ">= 2.1", "< 2.2" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails7.2_graphql2.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "devise_token_auth", ">= 1.2.1" 7 | gem "devise", ">= 4.7" 8 | gem "rails", git: "https://github.com/rails/rails", branch: "7-2-stable" 9 | gem "graphql", ">= 2.2", "< 2.3" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails7.2_graphql2.3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "devise_token_auth", ">= 1.2.1" 7 | gem "devise", ">= 4.7" 8 | gem "rails", git: "https://github.com/rails/rails", branch: "7-2-stable" 9 | gem "graphql", ">= 2.3", "< 2.4" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails7.2_graphql2.4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "devise_token_auth", ">= 1.2.1" 7 | gem "devise", ">= 4.7" 8 | gem "rails", git: "https://github.com/rails/rails", branch: "7-2-stable" 9 | gem "graphql", ">= 2.4", "< 2.5" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /spec/dummy/app/models/schema_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SchemaUser < ApplicationRecord 4 | devise :database_authenticatable, 5 | :recoverable, 6 | :trackable, 7 | :validatable, 8 | :confirmable 9 | 10 | include GraphqlDevise::Authenticatable 11 | 12 | validates :name, presence: true 13 | end 14 | -------------------------------------------------------------------------------- /bin/install_bundler.rb: -------------------------------------------------------------------------------- 1 | #!ruby 2 | 3 | ruby_version = Gem::Version.new(RUBY_VERSION) 4 | 5 | if ruby_version < Gem::Version.new('2.6') 6 | system('gem install bundler -v 2.3.27') 7 | elsif ruby_version >= Gem::Version.new('2.6') && ruby_version < Gem::Version.new('3.0') 8 | system('gem install bundler -v 2.4.22') 9 | else 10 | system('gem install bundler') 11 | end 12 | -------------------------------------------------------------------------------- /spec/factories/schema_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :schema_user do 5 | name { Faker::FunnyName.two_word_name } 6 | email { Faker::Internet.unique.email } 7 | password { Faker::Internet.password } 8 | 9 | trait :confirmed do 10 | confirmed_at { Time.zone.now } 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /gemfiles/rails6.1_graphql1.11.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sqlite3", "~> 1.5.4" 6 | gem "public_suffix", "< 5" 7 | gem "devise", ">= 4.7" 8 | gem "rails", git: "https://github.com/rails/rails", branch: "6-1-stable" 9 | gem "graphql", "~> 1.11.0" 10 | gem "factory_bot", "<= 6.4.4" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails6.1_graphql1.12.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sqlite3", "~> 1.5.4" 6 | gem "public_suffix", "< 5" 7 | gem "devise", ">= 4.7" 8 | gem "rails", git: "https://github.com/rails/rails", branch: "6-1-stable" 9 | gem "graphql", "~> 1.12.0" 10 | gem "factory_bot", "<= 6.4.4" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails6.1_graphql2.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "public_suffix", "< 5" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise", ">= 4.7" 8 | gem "rails", git: "https://github.com/rails/rails", branch: "6-1-stable" 9 | gem "graphql", "~> 2.0.1" 10 | gem "factory_bot", "<= 6.4.4" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails7.0_graphql2.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise_token_auth", ">= 1.2.1" 8 | gem "devise", ">= 4.7" 9 | gem "rails", git: "https://github.com/rails/rails", branch: "7-0-stable" 10 | gem "graphql", ">= 2.0", "< 2.1" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails7.0_graphql2.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise_token_auth", ">= 1.2.1" 8 | gem "devise", ">= 4.7" 9 | gem "rails", git: "https://github.com/rails/rails", branch: "7-0-stable" 10 | gem "graphql", ">= 2.1", "< 2.2" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails7.0_graphql2.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise_token_auth", ">= 1.2.1" 8 | gem "devise", ">= 4.7" 9 | gem "rails", git: "https://github.com/rails/rails", branch: "7-0-stable" 10 | gem "graphql", ">= 2.2", "< 2.3" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails7.0_graphql2.3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise_token_auth", ">= 1.2.1" 8 | gem "devise", ">= 4.7" 9 | gem "rails", git: "https://github.com/rails/rails", branch: "7-0-stable" 10 | gem "graphql", ">= 2.3", "< 2.4" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails7.0_graphql2.4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise_token_auth", ">= 1.2.1" 8 | gem "devise", ">= 4.7" 9 | gem "rails", git: "https://github.com/rails/rails", branch: "7-0-stable" 10 | gem "graphql", ">= 2.4", "< 2.5" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails7.1_graphql2.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise_token_auth", ">= 1.2.1" 8 | gem "devise", ">= 4.7" 9 | gem "rails", git: "https://github.com/rails/rails", branch: "7-1-stable" 10 | gem "graphql", ">= 2.0", "< 2.1" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails7.1_graphql2.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise_token_auth", ">= 1.2.1" 8 | gem "devise", ">= 4.7" 9 | gem "rails", git: "https://github.com/rails/rails", branch: "7-1-stable" 10 | gem "graphql", ">= 2.1", "< 2.2" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails7.1_graphql2.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise_token_auth", ">= 1.2.1" 8 | gem "devise", ">= 4.7" 9 | gem "rails", git: "https://github.com/rails/rails", branch: "7-1-stable" 10 | gem "graphql", ">= 2.2", "< 2.3" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails7.1_graphql2.3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise_token_auth", ">= 1.2.1" 8 | gem "devise", ">= 4.7" 9 | gem "rails", git: "https://github.com/rails/rails", branch: "7-1-stable" 10 | gem "graphql", ">= 2.3", "< 2.4" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails7.1_graphql2.4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sassc-rails" 6 | gem "sqlite3", "~> 1.5.4" 7 | gem "devise_token_auth", ">= 1.2.1" 8 | gem "devise", ">= 4.7" 9 | gem "rails", git: "https://github.com/rails/rails", branch: "7-1-stable" 10 | gem "graphql", ">= 2.4", "< 2.5" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/options_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | class OptionsValidator 6 | def initialize(validators = []) 7 | @validators = validators 8 | end 9 | 10 | def validate! 11 | @validators.each(&:validate!) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/graphql_devise/concerns/authenticatable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Authenticatable 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | include DeviseTokenAuth::Concerns::User 9 | include AdditionalModelMethods 10 | 11 | ::GraphqlDevise.configure_warden_serializer_for_model(self) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/graphql_devise/concerns/field_authentication.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module FieldAuthentication 5 | extend ActiveSupport::Concern 6 | 7 | def initialize(*args, authenticate: nil, **kwargs, &block) 8 | @authenticate = authenticate 9 | super(*args, **kwargs, &block) 10 | end 11 | 12 | attr_reader :authenticate 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "graphql_devise" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /spec/support/requests/json_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Requests 4 | module JsonHelpers 5 | def json_response 6 | parsed_response = JSON.parse(response.body) 7 | 8 | if parsed_response.instance_of?(Array) 9 | parsed_response.map(&:deep_symbolize_keys) 10 | else 11 | parsed_response.deep_symbolize_keys 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/mutations/register.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class Register < GraphqlDevise::Mutations::Register 5 | argument :name, String, required: false 6 | 7 | field :user, Types::UserType, null: true 8 | 9 | def resolve(email:, **attrs) 10 | original_payload = super 11 | original_payload.merge(user: original_payload[:authenticatable]) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/mutations/reset_admin_password_with_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class ResetAdminPasswordWithToken < GraphqlDevise::Mutations::UpdatePasswordWithToken 5 | field :authenticatable, Types::AdminType, null: false 6 | 7 | def resolve(reset_password_token:, **attrs) 8 | super do |admin| 9 | controller.sign_in(admin) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/graphql_devise/errors/detailed_user_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | class DetailedUserError < ExecutionError 5 | def initialize(message, errors:) 6 | @message = message 7 | @errors = errors 8 | 9 | super(message) 10 | end 11 | 12 | def to_h 13 | super.merge(extensions: { code: ERROR_CODES.fetch(:user_error), detailed_errors: @errors }) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/dummy/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file should contain all the record creation needed to seed the database with its default values. 4 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 5 | # 6 | # Examples: 7 | # 8 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 9 | # Character.create(name: 'Luke', movie: movies.first) 10 | -------------------------------------------------------------------------------- /lib/graphql_devise/resolvers/dummy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Resolvers 5 | class Dummy < Base 6 | type String, null: false 7 | description 'Field necessary as at least one query must be present in the schema' 8 | 9 | def resolve 10 | 'Dummy field necessary as graphql-ruby gem requires at least one query to be present in the schema.' 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/mutations/login.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class Login < GraphqlDevise::Mutations::Login 5 | field :user, Types::UserType, null: true 6 | 7 | def resolve(email:, password:) 8 | original_payload = super do |user| 9 | user.do_something 10 | user.reload 11 | end 12 | 13 | original_payload.merge(user: original_payload[:authenticatable]) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | xJMNgtGXaXu/5+ox81/k6kAET+XLzWCxvl/qk94eRjjxXxel/uspj6ylRR3WWgHll8u6OP6tgbY3Kj8rZxca5XaUV/g11u8MvkQI0LHUEX7xxuoAM4GBDoh9Uye873DmKrdFQ323Tu7zZyPnjFkZoEzVwzpgsd6Skdxzk+OJmzGEBsSWZGB5UYwmFaIO4V+TeSNn/WTeOEny93W2wy9PCuEKUN7DEkBfTdaoz9stwd4PeHJcNkUw1f42c19ZMvINzx1+2VTXKhCuT+ppG2M57U8Azg1JLSTbggKXIU4xWlvkgu52b3kswT2IEtXca+zG6hp6wddLgklx/+n7Q16HR1qWHb3/IxKdGOlMHPonDsT13xq59HVh1OVvu3wUe1XKAWaOxWIuZn6E1qyRnwl7sR4TK0vMzhIYtbe7--ogdQ8+HVSUjzpGAv--tp73NY+aZS1nyiuNvnAe2A== -------------------------------------------------------------------------------- /spec/dummy/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/operation_preparers/resource_klass_setter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OperationPreparers 6 | class ResourceKlassSetter 7 | def initialize(klass) 8 | @klass = klass 9 | end 10 | 11 | def call(operation, **) 12 | operation.instance_variable_set(:@resource_klass, @klass) 13 | 14 | operation 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 6 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 7 | 8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 9 | # Rails.backtrace_cleaner.remove_silencers! 10 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :user do 5 | name { Faker::FunnyName.two_word_name } 6 | email { Faker::Internet.unique.email } 7 | password { Faker::Internet.password } 8 | 9 | trait :confirmed do 10 | confirmed_at { Time.now } 11 | end 12 | 13 | trait :locked do 14 | locked_at { Time.now } 15 | end 16 | 17 | trait :auth_unavailable do 18 | auth_available { false } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/types/mutation_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class MutationType < BaseObject 5 | field_class GraphqlDevise::Types::BaseField if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('2.0') 6 | 7 | field :dummy_mutation, String, null: false, authenticate: true 8 | field :update_user, mutation: Mutations::UpdateUser 9 | 10 | def dummy_mutation 11 | 'Necessary so GraphQL gem does not complain about empty mutation type' 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/operation_preparers/resolver_type_setter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OperationPreparers 6 | class ResolverTypeSetter 7 | def initialize(authenticatable_type) 8 | @authenticatable_type = authenticatable_type 9 | end 10 | 11 | def call(resolver, **) 12 | resolver.type(@authenticatable_type, null: false) 13 | 14 | resolver 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/mutations/update_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class UpdateUser < BaseMutation 5 | field :user, Types::UserType, null: false 6 | 7 | argument :email, String, required: false 8 | argument :name, String, required: false 9 | 10 | def resolve(**attrs) 11 | user = context[:current_resource] 12 | 13 | user.update_with_email( 14 | attrs.merge(confirmation_url: 'https://google.com') 15 | ) 16 | 17 | { user: user } 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /.bundle/ 4 | /.yardoc 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | 12 | README.md.* 13 | 14 | # rspec failure tracking 15 | .rspec_status 16 | /spec/dummy/log/ 17 | /spec/dummy/tmp/ 18 | /Gemfile.lock 19 | *.gemfile.lock 20 | *.sqlite3 21 | *.sqlite3-journal 22 | /spec/dummy/db/development.sqlite3 23 | /spec/dummy/db/test.sqlite3 24 | /*.gem 25 | 26 | # rvm config files 27 | .ruby-version 28 | .ruby-gemset 29 | .tool-versions 30 | 31 | .env 32 | /spec/tmp/config/routes.rb 33 | /.byebug_history 34 | -------------------------------------------------------------------------------- /spec/support/contexts/schema_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'with graphql schema test' do 4 | let(:variables) { {} } 5 | let(:resource_names) { [:user] } 6 | let(:resource) { nil } 7 | let(:controller) { instance_double(GraphqlDevise::GraphqlController) } 8 | let(:context) do 9 | { current_resource: resource, controller: controller, resource_name: resource_names } 10 | end 11 | let(:response) do 12 | schema.execute(query, context: context, variables: variables).deep_symbolize_keys 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ApplicationRecord 4 | devise :database_authenticatable, 5 | :registerable, 6 | :recoverable, 7 | :rememberable, 8 | :trackable, 9 | :lockable, 10 | :validatable, 11 | :confirmable 12 | 13 | include GraphqlDevise::Authenticatable 14 | 15 | validates :name, presence: true 16 | 17 | def valid_for_authentication? 18 | auth_available && super 19 | end 20 | 21 | def do_something 22 | 'Nothing to see here!' 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/services/mount_method/operation_preparers/resource_klass_setter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::ResourceKlassSetter do 6 | describe '#call' do 7 | subject(:prepared_operation) { described_class.new(model).call(operation) } 8 | 9 | let(:operation) { double(:operation) } 10 | let(:model) { User } 11 | 12 | it 'sets a gql name to the operation' do 13 | expect(prepared_operation.instance_variable_get(:@resource_klass)).to eq(model) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('..', __dir__) 6 | ENGINE_PATH = File.expand_path('../lib/graphql_devise/engine', __dir__) 7 | APP_PATH = File.expand_path('../spec/dummy/config/application', __dir__) 8 | 9 | # Set up gems listed in the Gemfile. 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 11 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 12 | 13 | require 'rails/all' 14 | require 'rails/engine/commands' 15 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # This file contains settings for ActionController::ParamsWrapper which 6 | # is enabled by default. 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) do 10 | wrap_parameters format: [:json] 11 | end 12 | 13 | # To enable root element in JSON for ActiveRecord objects. 14 | # ActiveSupport.on_load(:active_record) do 15 | # self.include_root_in_json = true 16 | # end 17 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/operation_preparers/gql_name_setter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OperationPreparers 6 | class GqlNameSetter 7 | def initialize(mapping_name) 8 | @mapping_name = mapping_name 9 | end 10 | 11 | def call(operation, **) 12 | operation.graphql_name(graphql_name) 13 | 14 | operation 15 | end 16 | 17 | private 18 | 19 | def graphql_name 20 | @mapping_name.camelize(:upper) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/option_sanitizer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | class OptionSanitizer 6 | def initialize(options = {}, supported_options = MountMethod::SUPPORTED_OPTIONS) 7 | @options = options 8 | @supported_options = supported_options 9 | end 10 | 11 | def call! 12 | @supported_options.each_with_object(Struct.new(*@supported_options.keys).new) do |(key, checker), result| 13 | result[key] = checker.call!(@options[key], key) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/services/mount_method/operation_preparers/gql_name_setter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::GqlNameSetter do 6 | describe '#call' do 7 | subject(:prepared_operation) { described_class.new(mapping_name).call(operation) } 8 | 9 | let(:operation) { double(:operation) } 10 | let(:mapping_name) { 'user_login' } 11 | 12 | it 'sets a gql name to the operation' do 13 | expect(operation).to receive(:graphql_name).with('UserLogin') 14 | 15 | prepared_operation 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/contexts/graphql_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'with graphql query request' do 4 | let(:headers) { {} } 5 | let(:variables) { {} } 6 | let(:graphql_params) do 7 | { params: { query: query, variables: variables }, headers: headers } 8 | end 9 | 10 | def post_request(path = '/api/v1/graphql_auth') 11 | send_request(path, :post) 12 | end 13 | 14 | def get_request(path = '/api/v1/graphql_auth') 15 | send_request(path, :get) 16 | end 17 | 18 | def send_request(path, method) 19 | public_send(method, path, **graphql_params) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/services/mount_method/operation_preparers/resolver_type_setter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::ResolverTypeSetter do 6 | describe '#call' do 7 | subject(:prepared_operation) { described_class.new(field_type).call(operation) } 8 | 9 | let(:operation) { double(:operation) } 10 | let(:field_type) { double(:type) } 11 | 12 | it 'sets a field for the mutation' do 13 | expect(operation).to receive(:type).with(field_type, null: false) 14 | 15 | prepared_operation 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/operation_preparers/mutation_field_setter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OperationPreparers 6 | class MutationFieldSetter 7 | def initialize(authenticatable_type) 8 | @authenticatable_type = authenticatable_type 9 | end 10 | 11 | def call(mutation, authenticatable: true) 12 | return mutation unless authenticatable 13 | 14 | mutation.field(:authenticatable, @authenticatable_type, null: false) 15 | mutation 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/graphql_devise/mutations/logout.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Mutations 5 | class Logout < Base 6 | def resolve 7 | if current_resource && client && current_resource.tokens[client] 8 | current_resource.tokens.delete(client) 9 | current_resource.save! 10 | 11 | remove_resource 12 | 13 | yield current_resource if block_given? 14 | 15 | { authenticatable: current_resource } 16 | else 17 | raise_user_error(I18n.t('graphql_devise.user_not_found')) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Avoid CORS issues when API is called from the frontend app. 6 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 7 | 8 | # Read more: https://github.com/cyu/rack-cors 9 | 10 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 11 | # allow do 12 | # origins 'example.com' 13 | # 14 | # resource '*', 15 | # headers: :any, 16 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 17 | # end 18 | # end 19 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | GraphqlDevise::Engine.routes.draw do 4 | # Required as Devise forces routes to reload on eager_load 5 | unless GraphqlDevise.schema_loaded? 6 | if GraphqlDevise::Types::QueryType.fields.blank? 7 | GraphqlDevise::Types::QueryType.field(:dummy, resolver: GraphqlDevise::Resolvers::Dummy) 8 | end 9 | 10 | if GraphqlDevise::Types::MutationType.fields.present? 11 | GraphqlDevise::Schema.mutation(GraphqlDevise::Types::MutationType) 12 | end 13 | 14 | GraphqlDevise::Schema.query(GraphqlDevise::Types::QueryType) 15 | 16 | GraphqlDevise.load_schema 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/option_validators/skip_only_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OptionValidators 6 | class SkipOnlyValidator 7 | def initialize(options:) 8 | @options = options 9 | end 10 | 11 | def validate! 12 | if [@options.skip, @options.only].all?(&:present?) 13 | raise( 14 | InvalidMountOptionsError, 15 | "Can't specify both `skip` and `only` options when mounting the route." 16 | ) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200321121807_create_users_customers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateUsersCustomers < ActiveRecord::Migration[6.0] 4 | def change 5 | create_table :users_customers do |t| 6 | ## Required 7 | t.string :provider, null: false, default: 'email' 8 | t.string :uid, null: false, default: '' 9 | 10 | ## Database authenticatable 11 | t.string :encrypted_password, null: false, default: '' 12 | 13 | ## User Info 14 | t.string :email 15 | 16 | ## Tokens 17 | t.text :tokens 18 | 19 | t.string :name, null: false 20 | 21 | t.timestamps 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/interpreter_schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class InterpreterSchema < GraphQL::Schema 4 | if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.9.0') && Gem::Version.new(GraphQL::VERSION) < Gem::Version.new('2.0') 5 | use GraphQL::Execution::Interpreter 6 | end 7 | if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.10.0') && Gem::Version.new(GraphQL::VERSION) < Gem::Version.new('2.0') 8 | use GraphQL::Analysis::AST 9 | end 10 | 11 | use GraphqlDevise::SchemaPlugin.new(query: Types::QueryType, authenticate_default: false) 12 | 13 | mutation(Types::MutationType) 14 | query(Types::QueryType) 15 | end 16 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/option_sanitizers/string_checker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OptionSanitizers 6 | class StringChecker 7 | def initialize(default_string = nil) 8 | @default_string = default_string 9 | end 10 | 11 | def call!(value, key) 12 | return @default_string if value.blank? 13 | 14 | unless value.instance_of?(String) 15 | raise InvalidMountOptionsError, "`#{key}` option has an invalid value. String expected." 16 | end 17 | 18 | value 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/graphql_devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |<%= t(:hello).capitalize %> <%= @resource.email %>!
2 | 3 |<%= t('.request_reset_link_msg') %>
4 | 5 |6 | <% if message['schema_url'].present? %> 7 | <%= 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}" %> 8 | <% else %> 9 | <%= link_to t('.password_change_link'), "#{message['redirect-url'].to_s}?#{{ reset_password_token: @token }.to_query}" %> 10 | <% end %> 11 |
12 | 13 |<%= t('.ignore_mail_msg') %>
14 |<%= t('.no_changes_msg') %>
15 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/dummy_schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DummySchema < GraphQL::Schema 4 | use GraphqlDevise::SchemaPlugin.new( 5 | query: Types::QueryType, 6 | mutation: Types::MutationType, 7 | public_introspection: true, 8 | resource_loaders: [ 9 | GraphqlDevise::ResourceLoader.new( 10 | User, 11 | only: [ 12 | :login, 13 | :resend_confirmation_with_token 14 | ] 15 | ), 16 | GraphqlDevise::ResourceLoader.new(Guest, only: [:logout]), 17 | GraphqlDevise::ResourceLoader.new(SchemaUser) 18 | ] 19 | ) 20 | 21 | mutation(Types::MutationType) 22 | query(Types::QueryType) 23 | end 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV['CI'] && !ENV['SKIP_COVERALLS'] 4 | require 'simplecov' 5 | require 'coveralls' 6 | 7 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter 8 | SimpleCov.start 'rails' do 9 | add_filter ['spec'] 10 | end 11 | end 12 | 13 | require 'pry' 14 | require 'bundler/setup' 15 | require 'graphql_devise' 16 | 17 | RSpec.configure do |config| 18 | # Enable flags like --only-failures and --next-failure 19 | config.example_status_persistence_file_path = '.rspec_status' 20 | 21 | # Disable RSpec exposing methods globally on `Module` and `main` 22 | config.disable_monkey_patching! 23 | 24 | config.expect_with :rspec do |c| 25 | c.syntax = :expect 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/graphql_devise/concerns/additional_model_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module AdditionalModelMethods 5 | extend ActiveSupport::Concern 6 | 7 | class_methods do 8 | def reconfirmable 9 | column_attributes = try(:column_names) || [] 10 | fields_attributes = try(:fields)&.keys || [] 11 | has_unconfirmed_email_attr = column_attributes.include?('unconfirmed_email') || fields_attributes.include?('unconfirmed_email') 12 | devise_modules.include?(:confirmable) && has_unconfirmed_email_attr 13 | end 14 | end 15 | 16 | def update_with_email(attributes = {}) 17 | GraphqlDevise::Model::WithEmailUpdater.new(self, attributes).call 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new inflection rules using the following format. Inflections 6 | # are locale specific, and you may define rules for as many different 7 | # locales as you wish. All of these examples are active by default: 8 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 9 | # inflect.plural /^(ox)$/i, '\1en' 10 | # inflect.singular /^(ox)en/i, '\1' 11 | # inflect.irregular 'person', 'people' 12 | # inflect.uncountable %w( fish sheep ) 13 | # end 14 | 15 | # These inflection rules are supported but not enabled by default: 16 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 17 | # inflect.acronym 'RESTful' 18 | # end 19 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/operation_sanitizer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | class OperationSanitizer 6 | def self.call(default:, only:, skipped:) 7 | new( 8 | default: default, 9 | only: only, 10 | skipped: skipped 11 | ).call 12 | end 13 | 14 | def initialize(default:, only:, skipped:) 15 | @default = default 16 | @only = only 17 | @skipped = skipped 18 | end 19 | 20 | def call 21 | operations = if @only.present? 22 | @default.slice(*@only) 23 | elsif @skipped.present? 24 | @default.except(*@skipped) 25 | else 26 | @default 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/supported_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | SUPPORTED_OPTIONS = { 6 | at: OptionSanitizers::StringChecker.new('/graphql_auth'), 7 | operations: OptionSanitizers::HashChecker.new([GraphQL::Schema::Resolver, GraphQL::Schema::Mutation]), 8 | only: OptionSanitizers::ArrayChecker.new(Symbol), 9 | skip: OptionSanitizers::ArrayChecker.new(Symbol), 10 | additional_queries: OptionSanitizers::HashChecker.new(GraphQL::Schema::Resolver), 11 | additional_mutations: OptionSanitizers::HashChecker.new(GraphQL::Schema::Mutation), 12 | authenticatable_type: OptionSanitizers::ClassChecker.new(GraphQL::Schema::Member) 13 | }.freeze 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/mutations/register_confirmed_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class RegisterConfirmedUser < GraphqlDevise::Mutations::Base 5 | argument :email, String, required: true 6 | argument :name, String, required: true 7 | argument :password, String, required: true 8 | argument :password_confirmation, String, required: true 9 | 10 | field :user, Types::UserType, null: true 11 | 12 | def resolve(**attrs) 13 | user = User.new(attrs.merge(confirmed_at: Time.zone.now)) 14 | 15 | if user.save 16 | { user: user } 17 | else 18 | raise_user_error_list( 19 | 'Custom registration failed', 20 | resource: user 21 | ) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/requests/mutations/additional_queries_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'Additional Queries' do 6 | include_context 'with graphql query request' 7 | 8 | let(:public_user) { create(:user, :confirmed) } 9 | 10 | context 'when using the user model' do 11 | let(:query) do 12 | <<-GRAPHQL 13 | query { 14 | publicUser( 15 | id: #{public_user.id} 16 | ) { 17 | email 18 | name 19 | } 20 | } 21 | GRAPHQL 22 | end 23 | 24 | before { post_request } 25 | 26 | it 'fetches a user by ID' do 27 | expect(json_response[:data][:publicUser]).to include( 28 | email: public_user.email, 29 | name: public_user.name 30 | ) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/graphql_devise/route_mounter.rb: -------------------------------------------------------------------------------- 1 | module GraphqlDevise 2 | module RouteMounter 3 | def mount_graphql_devise_for(resource, options = {}) 4 | routing = 'graphql_devise/graphql#auth' 5 | 6 | if (base_controller = options.delete(:base_controller)) 7 | new_controller = GraphqlDevise.const_set("#{resource}AuthController", Class.new(base_controller)) 8 | new_controller.include(SetUserByToken) 9 | new_controller.include(AuthControllerMethods) 10 | 11 | routing = "#{new_controller.to_s.underscore.gsub('_controller','')}#auth" 12 | end 13 | 14 | clean_options = ResourceLoader.new(resource, options, true).call( 15 | Types::QueryType, 16 | Types::MutationType 17 | ) 18 | 19 | post clean_options.at, to: routing 20 | get clean_options.at, to: routing 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/option_sanitizers/hash_checker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OptionSanitizers 6 | class HashChecker 7 | def initialize(element_type_array) 8 | @element_type_array = Array(element_type_array) 9 | @default_value = {} 10 | end 11 | 12 | def call!(value, key) 13 | return @default_value if value.blank? 14 | 15 | unless value.instance_of?(Hash) 16 | raise InvalidMountOptionsError, "`#{key}` option has an invalid value. Hash expected. Got #{value.class}." 17 | end 18 | 19 | value.each { |internal_key, klass| ClassChecker.new(@element_type_array).call!(klass, "#{key} -> #{internal_key}") } 20 | 21 | value 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement 3 | about: Suggest an idea for improving the gem 4 | labels: 'issue: enhancement' 5 | --- 6 | 7 | ### What is the problem the enhancement will solve? 8 | 9 | 12 | 13 | (Write the problem here.) 14 | 15 | ### Describe the solution you have in mind 16 | 17 | 21 | 22 | (Describe your proposed solution here.) 23 | 24 | ### Describe alternatives you've considered 25 | 26 | 29 | 30 | (Write your findings here.) 31 | 32 | ### Additional context 33 | 34 | 37 | 38 | (Write your answer here.) 39 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/option_sanitizers/array_checker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OptionSanitizers 6 | class ArrayChecker 7 | def initialize(element_type) 8 | @element_type = element_type 9 | @default_value = [] 10 | end 11 | 12 | def call!(value, key) 13 | return @default_value if value.blank? 14 | 15 | unless value.instance_of?(Array) 16 | raise InvalidMountOptionsError, "`#{key}` option has an invalid value. Array expected." 17 | end 18 | 19 | unless value.all? { |element| element.instance_of?(@element_type) } 20 | raise InvalidMountOptionsError, "`#{key}` option has invalid elements. #{@element_type} expected." 21 | end 22 | 23 | value 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/option_sanitizers/class_checker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OptionSanitizers 6 | class ClassChecker 7 | def initialize(klass) 8 | @klass_array = Array(klass) 9 | end 10 | 11 | def call!(value, key) 12 | return if value.nil? 13 | 14 | unless value.instance_of?(Class) 15 | raise InvalidMountOptionsError, "`#{key}` option has an invalid value. Class expected." 16 | end 17 | 18 | unless @klass_array.any? { |klass| value.ancestors.include?(klass) } 19 | raise InvalidMountOptionsError, 20 | "`#{key}` option has an invalid value. #{@klass_array.join(', ')} or descendants expected. Got #{value}." 21 | end 22 | 23 | value 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/dummy/bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | require 'pathname' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /spec/services/schema_plugin_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::SchemaPlugin do 6 | describe '#call' do 7 | subject(:plugin) { described_class.new(query: query, mutation: mutation, resource_loaders: loaders) } 8 | 9 | let(:query) { instance_double(GraphQL::Schema::Object) } 10 | let(:mutation) { instance_double(GraphQL::Schema::Object) } 11 | 12 | context 'when loaders are not provided' do 13 | let(:loaders) { [] } 14 | 15 | it 'does not fail' do 16 | expect { plugin }.not_to raise_error 17 | end 18 | end 19 | 20 | context 'when a loaders is not an instance of loader' do 21 | let(:loaders) { ['not a loader instance'] } 22 | 23 | it 'raises an error' do 24 | expect { plugin }.to raise_error(GraphqlDevise::Error, 'Invalid resource loader instance') 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/graphql_devise/default_operations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module DefaultOperations 5 | QUERIES = {}.freeze 6 | MUTATIONS = { 7 | login: { klass: Mutations::Login, authenticatable: true }, 8 | logout: { klass: Mutations::Logout, authenticatable: true }, 9 | register: { klass: Mutations::Register, authenticatable: true }, 10 | update_password_with_token: { klass: Mutations::UpdatePasswordWithToken, authenticatable: true }, 11 | send_password_reset_with_token: { klass: Mutations::SendPasswordResetWithToken, authenticatable: false }, 12 | resend_confirmation_with_token: { klass: Mutations::ResendConfirmationWithToken, authenticatable: false }, 13 | confirm_registration_with_token: { klass: Mutations::ConfirmRegistrationWithToken, authenticatable: true } 14 | }.freeze 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/option_validators/supported_operations_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OptionValidators 6 | class SupportedOperationsValidator 7 | def initialize(provided_operations: [], supported_operations: [], key:) 8 | @provided_operations = provided_operations 9 | @supported_operations = supported_operations 10 | @key = key 11 | end 12 | 13 | def validate! 14 | unsupported_operations = @provided_operations - @supported_operations 15 | 16 | if unsupported_operations.present? 17 | raise( 18 | InvalidMountOptionsError, 19 | "#{@key} option contains unsupported operations: \"#{unsupported_operations.join(', ')}\". Check for typos." 20 | ) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/graphql_devise/types/credential_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Types 5 | class CredentialType < BaseType 6 | field :access_token, String, null: false 7 | field :uid, String, null: false 8 | field :token_type, String, null: false 9 | field :client, String, null: false 10 | field :expiry, Int, null: false 11 | 12 | def access_token 13 | object[DeviseTokenAuth.headers_names[:"access-token"]] 14 | end 15 | 16 | def uid 17 | object[DeviseTokenAuth.headers_names[:uid]] 18 | end 19 | 20 | def token_type 21 | object[DeviseTokenAuth.headers_names[:"token-type"]] 22 | end 23 | 24 | def client 25 | object[DeviseTokenAuth.headers_names[:client]] 26 | end 27 | 28 | def expiry 29 | object[DeviseTokenAuth.headers_names[:expiry]] 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/dummy/app/graphql/types/query_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class QueryType < BaseObject 5 | field_class GraphqlDevise::Types::BaseField if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('2.0') 6 | 7 | field :user, resolver: Resolvers::UserShow 8 | field :public_field, String, null: false, authenticate: false 9 | field :private_field, String, null: false, authenticate: true 10 | field :vip_field, String, null: false, authenticate: ->(user) { user.is_a?(User) && user.vip? } 11 | 12 | def public_field 13 | if context[:current_resource] 14 | "Authenticated user on public field: #{context[:current_resource].email}" 15 | else 16 | 'Field does not require authentication' 17 | end 18 | end 19 | 20 | def private_field 21 | 'Field will always require authentication' 22 | end 23 | 24 | def vip_field 25 | 'Field available only for VIP Users' 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/option_validators/provided_operations_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OptionValidators 6 | class ProvidedOperationsValidator 7 | def initialize(options:, supported_operations:) 8 | @options = options 9 | @supported_operations = supported_operations 10 | end 11 | 12 | def validate! 13 | supported_keys = @supported_operations.keys 14 | 15 | [ 16 | SupportedOperationsValidator.new(provided_operations: @options.skip, key: :skip, supported_operations: supported_keys), 17 | SupportedOperationsValidator.new(provided_operations: @options.only, key: :only, supported_operations: supported_keys), 18 | SupportedOperationsValidator.new(provided_operations: @options.operations.keys, key: :operations, supported_operations: supported_keys) 19 | ].each(&:validate!) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/graphql_devise/mutations/confirm_registration_with_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Mutations 5 | class ConfirmRegistrationWithToken < Base 6 | argument :confirmation_token, String, required: true 7 | 8 | field :credentials, 9 | Types::CredentialType, 10 | null: true, 11 | description: 'Authentication credentials. Null unless user is signed in after confirmation.' 12 | 13 | def resolve(confirmation_token:) 14 | resource = resource_class.confirm_by_token(confirmation_token) 15 | 16 | if resource.errors.empty? 17 | yield resource if block_given? 18 | 19 | response_payload = { authenticatable: resource } 20 | 21 | response_payload[:credentials] = generate_auth_headers(resource) if resource.active_for_authentication? 22 | 23 | response_payload 24 | else 25 | raise_user_error(I18n.t('graphql_devise.confirmations.invalid_token')) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | require 'pathname' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /spec/services/mount_method/option_sanitizers/string_checker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OptionSanitizers::StringChecker do 6 | describe '#call!' do 7 | subject(:clean_value) { described_class.new(default_string).call!(value, key) } 8 | 9 | let(:key) { :any_option } 10 | let(:default_string) { 'default string' } 11 | 12 | context 'when no value is provided' do 13 | let(:value) { nil } 14 | 15 | it { is_expected.to eq(default_string) } 16 | end 17 | 18 | context 'when provided value is not a String' do 19 | let(:value) { 1000 } 20 | 21 | it 'raises an error' do 22 | expect { clean_value }.to raise_error(GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has an invalid value. String expected.") 23 | end 24 | end 25 | 26 | context 'when provided array contains all valid elements' do 27 | let(:value) { 'custom valid string' } 28 | 29 | it { is_expected.to eq(value) } 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/services/mount_method/operation_preparers/mutation_field_setter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::MutationFieldSetter do 6 | describe '#call' do 7 | subject(:prepared_operation) { described_class.new(field_type).call(operation, authenticatable: authenticatable) } 8 | 9 | let(:operation) { double(:operation) } 10 | let(:field_type) { double(:type) } 11 | 12 | context 'when resource is authtenticable' do 13 | let(:authenticatable) { true } 14 | 15 | it 'sets a field for the mutation' do 16 | expect(operation).to receive(:field).with(:authenticatable, field_type, null: false) 17 | 18 | prepared_operation 19 | end 20 | end 21 | 22 | context 'when resource is *NOT* authtenticable' do 23 | let(:authenticatable) { false } 24 | 25 | it 'does *NOT* set a field for the mutation' do 26 | expect(operation).not_to receive(:field) 27 | 28 | prepared_operation 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'bundler/setup' 5 | rescue LoadError 6 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 7 | end 8 | 9 | require 'rdoc/task' 10 | 11 | RDoc::Task.new(:rdoc) do |rdoc| 12 | rdoc.rdoc_dir = 'rdoc' 13 | rdoc.title = 'GraphqlDevise' 14 | rdoc.options << '--line-numbers' 15 | rdoc.rdoc_files.include('README.md') 16 | rdoc.rdoc_files.include('lib/**/*.rb') 17 | end 18 | 19 | require 'github_changelog_generator/task' 20 | 21 | GitHubChangelogGenerator::RakeTask.new do |config| 22 | config.user = 'graphql-devise' 23 | config.project = 'graphql_devise' 24 | config.future_release = ENV['FUTURE_RELEASE'] 25 | config.add_issues_wo_labels = false 26 | config.add_pr_wo_labels = false 27 | end 28 | 29 | APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__) 30 | load 'rails/tasks/engine.rake' 31 | 32 | load 'rails/tasks/statistics.rake' 33 | 34 | require 'bundler/gem_tasks' 35 | 36 | require 'rspec/core/rake_task' 37 | 38 | RSpec::Core::RakeTask.new(:spec) 39 | 40 | task default: :spec 41 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/operation_preparers/custom_operation_preparer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OperationPreparers 6 | class CustomOperationPreparer 7 | def initialize(selected_keys:, custom_operations:, model:) 8 | @selected_keys = selected_keys 9 | @custom_operations = custom_operations 10 | @model = model 11 | end 12 | 13 | def call 14 | mapping_name = ::GraphqlDevise.to_mapping_name(@model) 15 | 16 | @custom_operations.slice(*@selected_keys).each_with_object({}) do |(action, operation), result| 17 | mapped_action = "#{mapping_name}_#{action}" 18 | 19 | result[mapped_action.to_sym] = [ 20 | OperationPreparers::GqlNameSetter.new(mapped_action), 21 | OperationPreparers::ResourceKlassSetter.new(@model) 22 | ].reduce(operation) { |prepared_operation, preparer| preparer.call(prepared_operation) } 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Mario Celi, David Revelo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/services/mount_method/operation_sanitizer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OperationSanitizer do 6 | describe '.call' do 7 | subject { described_class.call(default: default, only: only, skipped: skipped) } 8 | 9 | let(:op_class1) { Class.new } 10 | let(:op_class2) { Class.new } 11 | let(:op_class3) { Class.new } 12 | 13 | context 'when the operations passed are mutations' do 14 | let(:skipped) { [] } 15 | let(:only) { [] } 16 | let(:default) { { operation1: { klass: op_class1 }, operation2: { klass: op_class2 } } } 17 | 18 | context 'when no other option besides default is passed' do 19 | it { is_expected.to eq(default) } 20 | end 21 | 22 | context 'when there are only operations' do 23 | let(:only) { [:operation1] } 24 | 25 | it { is_expected.to eq(operation1: { klass: op_class1 }) } 26 | end 27 | 28 | context 'when there are skipped operations' do 29 | let(:skipped) { [:operation2] } 30 | 31 | it { is_expected.to eq(operation1: { klass: op_class1 }) } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20190916012505_create_admins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateAdmins < ActiveRecord::Migration[5.2] 4 | def change 5 | create_table :admins do |t| 6 | ## Required 7 | t.string :provider, null: false, default: 'email' 8 | t.string :uid, null: false, default: '' 9 | 10 | ## Database authenticatable 11 | t.string :encrypted_password, null: false, default: '' 12 | 13 | ## Recoverable 14 | t.string :reset_password_token 15 | t.datetime :reset_password_sent_at 16 | t.boolean :allow_password_change, default: false 17 | 18 | ## Confirmable 19 | t.string :confirmation_token 20 | t.datetime :confirmed_at 21 | t.datetime :confirmation_sent_at 22 | t.string :unconfirmed_email # Only if using reconfirmable 23 | 24 | ## User Info 25 | t.string :email 26 | 27 | ## Tokens 28 | t.text :tokens 29 | 30 | t.timestamps 31 | end 32 | 33 | add_index :admins, :email, unique: true 34 | add_index :admins, [:uid, :provider], unique: true 35 | add_index :admins, :reset_password_token, unique: true 36 | add_index :admins, :confirmation_token, unique: true 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20191013213045_create_guests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateGuests < ActiveRecord::Migration[6.0] 4 | def change 5 | create_table :guests do |t| 6 | ## Required 7 | t.string :provider, null: false, default: 'email' 8 | t.string :uid, null: false, default: '' 9 | 10 | ## Database authenticatable 11 | t.string :encrypted_password, null: false, default: '' 12 | 13 | ## Recoverable 14 | t.string :reset_password_token 15 | t.datetime :reset_password_sent_at 16 | t.boolean :allow_password_change, default: false 17 | 18 | ## Confirmable 19 | t.string :confirmation_token 20 | t.datetime :confirmed_at 21 | t.datetime :confirmation_sent_at 22 | t.string :unconfirmed_email # Only if using reconfirmable 23 | 24 | ## User Info 25 | t.string :email 26 | 27 | ## Tokens 28 | t.text :tokens 29 | 30 | t.timestamps 31 | end 32 | 33 | add_index :guests, :email, unique: true 34 | add_index :guests, [:uid, :provider], unique: true 35 | add_index :guests, :reset_password_token, unique: true 36 | add_index :guests, :confirmation_token, unique: true 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails' 4 | require 'active_model/railtie' 5 | require 'active_job/railtie' 6 | require 'active_record/railtie' 7 | require 'action_controller/railtie' 8 | require 'action_mailer/railtie' 9 | require 'action_view/railtie' 10 | 11 | # Require the gems listed in Gemfile, including any gems 12 | # you've limited to :test, :development, or :production. 13 | Bundler.require(*Rails.groups) 14 | require 'graphql_devise' 15 | 16 | module Dummy 17 | class Application < Rails::Application 18 | # Initialize configuration defaults for originally generated Rails version. 19 | # config.load_defaults 5.2 20 | 21 | # Settings in config/environments/* take precedence over those specified here. 22 | # Application configuration can go into files in config/initializers 23 | # -- all .rb files in that directory are automatically loaded after loading 24 | # the framework and any gems in your application. 25 | 26 | # Only loads a smaller set of middleware suitable for API only apps. 27 | # Middleware like session, flash, cookies can be added back manually. 28 | # Skip views, helpers and assets when generating a new resource. 29 | # config.api_only = true 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/services/mount_method/option_validators/skip_only_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OptionValidators::SkipOnlyValidator do 6 | describe '#validate!' do 7 | subject { -> { described_class.new(options: options).validate! } } 8 | 9 | context 'when only `only` key is set' do 10 | let(:options) { double(:clean_options, only: [:irrelevant], skip: []) } 11 | 12 | it { is_expected.not_to raise_error } 13 | end 14 | 15 | context 'when only `skip` key is set' do 16 | let(:options) { double(:clean_options, skip: [:irrelevant], only: []) } 17 | 18 | it { is_expected.not_to raise_error } 19 | end 20 | 21 | context 'when `skip` and `only` keys are set' do 22 | let(:options) { double(:clean_options, only: [:irrelevant], skip: [:irrelevant]) } 23 | 24 | it { is_expected.to raise_error(GraphqlDevise::InvalidMountOptionsError, "Can't specify both `skip` and `only` options when mounting the route.") } 25 | end 26 | 27 | context 'when neither `skip` nor `only are set`' do 28 | let(:options) { double(:clean_options, skip: [], only: []) } 29 | 30 | it { is_expected.not_to raise_error } 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/services/mount_method/operation_preparer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OperationPreparer do 6 | describe '#call' do 7 | subject(:prepared_operations) do 8 | described_class.new( 9 | model: model, 10 | selected_operations: selected, 11 | preparer: preparer, 12 | custom: custom, 13 | additional_operations: additional 14 | ).call 15 | end 16 | 17 | let(:logout_class) { Class.new(GraphQL::Schema::Resolver) } 18 | let(:model) { User } 19 | let(:preparer) { double(:preparer, call: logout_class) } 20 | let(:custom) { { login: double(:custom_login, graphql_name: nil) } } 21 | let(:additional) { { user_additional: double(:user_additional) } } 22 | let(:selected) do 23 | { 24 | login: { klass: double(:login_default) }, 25 | logout: { klass: logout_class } 26 | } 27 | end 28 | 29 | it 'is expected to return all provided operation keys' do 30 | expect(prepared_operations.keys).to contain_exactly( 31 | :user_login, 32 | :user_logout, 33 | :user_additional 34 | ) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | mount_graphql_devise_for( 5 | User, 6 | at: '/api/v1/graphql_auth', 7 | base_controller: CookiesController, 8 | operations: { login: Mutations::Login, register: Mutations::Register }, 9 | additional_mutations: { register_confirmed_user: Mutations::RegisterConfirmedUser }, 10 | additional_queries: { public_user: Resolvers::PublicUser } 11 | ) 12 | 13 | mount_graphql_devise_for( 14 | Admin, 15 | authenticatable_type: Types::CustomAdminType, 16 | skip: [:register], 17 | operations: { 18 | update_password_with_token: Mutations::ResetAdminPasswordWithToken 19 | }, 20 | at: '/api/v1/admin/graphql_auth' 21 | ) 22 | 23 | mount_graphql_devise_for( 24 | Guest, 25 | only: [:login, :logout, :register], 26 | at: '/api/v1/guest/graphql_auth' 27 | ) 28 | 29 | mount_graphql_devise_for( 30 | Users::Customer, 31 | only: [:login], 32 | at: '/api/v1/user_customer/graphql_auth' 33 | ) 34 | 35 | get '/api/v1/graphql', to: 'api/v1/graphql#graphql' 36 | post '/api/v1/graphql', to: 'api/v1/graphql#graphql' 37 | post '/api/v1/interpreter', to: 'api/v1/graphql#interpreter' 38 | end 39 | -------------------------------------------------------------------------------- /lib/graphql_devise/mutations/send_password_reset_with_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Mutations 5 | class SendPasswordResetWithToken < Base 6 | argument :email, String, required: true 7 | argument :redirect_url, String, required: true 8 | 9 | field :message, String, null: false 10 | 11 | def resolve(email:, redirect_url:) 12 | check_redirect_url_whitelist!(redirect_url) 13 | 14 | resource = find_resource(:email, get_case_insensitive_field(:email, email)) 15 | 16 | if resource 17 | yield resource if block_given? 18 | 19 | resource.send_reset_password_instructions( 20 | email: email, 21 | provider: 'email', 22 | redirect_url: redirect_url, 23 | template_path: ['graphql_devise/mailer'] 24 | ) 25 | 26 | if resource.errors.empty? 27 | { message: I18n.t('graphql_devise.passwords.send_instructions') } 28 | else 29 | raise_user_error_list(I18n.t('graphql_devise.invalid_resource'), resource: resource) 30 | end 31 | else 32 | raise_user_error(I18n.t('graphql_devise.user_not_found')) 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/graphql_devise/concerns/auth_controller_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module AuthControllerMethods 5 | extend ActiveSupport::Concern 6 | 7 | def auth 8 | result = if params[:_json] 9 | Schema.multiplex( 10 | params[:_json].map do |param| 11 | { query: param[:query] }.merge(execute_params(param)) 12 | end 13 | ) 14 | else 15 | Schema.execute(params[:query], **execute_params(params)) 16 | end 17 | 18 | render json: result unless performed? 19 | end 20 | 21 | attr_accessor :client_id, :token, :resource 22 | 23 | private 24 | 25 | def execute_params(item) 26 | { 27 | operation_name: item[:operationName], 28 | variables: ensure_hash(item[:variables]), 29 | context: { controller: self } 30 | } 31 | end 32 | 33 | def ensure_hash(ambiguous_param) 34 | case ambiguous_param 35 | when String 36 | if ambiguous_param.present? 37 | ensure_hash(JSON.parse(ambiguous_param)) 38 | else 39 | {} 40 | end 41 | when Hash, ActionController::Parameters 42 | ambiguous_param 43 | when nil 44 | {} 45 | else 46 | raise ArgumentError, "Unexpected parameter: #{ambiguous_param}" 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/services/mount_method/option_sanitizers/array_checker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OptionSanitizers::ArrayChecker do 6 | describe '#call!' do 7 | subject(:clean_value) { described_class.new(element_type).call!(value, key) } 8 | 9 | let(:key) { :any_option } 10 | let(:element_type) { Symbol } 11 | 12 | context 'when no value is provided' do 13 | let(:value) { nil } 14 | 15 | it { is_expected.to eq([]) } 16 | end 17 | 18 | context 'when provided value is not an array' do 19 | let(:value) { 'not an array' } 20 | 21 | it 'raises an error' do 22 | expect { clean_value }.to raise_error(GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has an invalid value. Array expected.") 23 | end 24 | end 25 | 26 | context 'when provided array contains invalid elements' do 27 | let(:value) { [:valid, 'invalid'] } 28 | 29 | it 'raises an error' do 30 | expect { clean_value }.to raise_error(GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has invalid elements. #{element_type} expected.") 31 | end 32 | end 33 | 34 | context 'when provided array contains all valid elements' do 35 | let(:value) { [:valid1, :valid2] } 36 | 37 | it { is_expected.to eq(value) } 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/operation_preparer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | class OperationPreparer 6 | def initialize(model:, selected_operations:, preparer:, custom:, additional_operations:) 7 | @selected_operations = selected_operations 8 | @preparer = preparer 9 | @model = model 10 | @custom = custom 11 | @additional_operations = additional_operations 12 | end 13 | 14 | def call 15 | default_operations = OperationPreparers::DefaultOperationPreparer.new( 16 | selected_operations: @selected_operations, 17 | custom_keys: @custom.keys, 18 | model: @model, 19 | preparer: @preparer 20 | ).call 21 | 22 | custom_operations = OperationPreparers::CustomOperationPreparer.new( 23 | selected_keys: @selected_operations.keys, 24 | custom_operations: @custom, 25 | model: @model 26 | ).call 27 | 28 | additional_operations = @additional_operations.each_with_object({}) do |(action, operation), result| 29 | result[action] = OperationPreparers::ResourceKlassSetter.new(@model).call(operation) 30 | end 31 | 32 | default_operations.merge(custom_operations).merge(additional_operations) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/graphql_devise/concerns/additional_controller_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module AdditionalControllerMethods 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | attr_accessor :client_id, :token, :resource 9 | end 10 | 11 | def gql_devise_context(*models) 12 | { 13 | current_resource: authenticate_model(*models), 14 | controller: self 15 | } 16 | end 17 | 18 | def authenticate_model(*models) 19 | models.each do |model| 20 | set_resource_by_token(model) 21 | return @resource if @resource.present? 22 | end 23 | 24 | nil 25 | end 26 | 27 | def resource_class(resource = nil) 28 | # Return the resource class instead of looking for a Devise mapping if resource is already a resource class 29 | return resource if resource.respond_to?(:find_by) 30 | 31 | super 32 | end 33 | 34 | def set_resource_by_token(resource) 35 | set_user_by_token(resource) 36 | end 37 | 38 | def build_redirect_headers(access_token, client, redirect_header_options = {}) 39 | { 40 | DeviseTokenAuth.headers_names[:'access-token'] => access_token, 41 | DeviseTokenAuth.headers_names[:client] => client, 42 | :config => params[:config], 43 | :client_id => client, 44 | :token => access_token 45 | }.merge(redirect_header_options) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200623003142_create_schema_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateSchemaUsers < ActiveRecord::Migration[6.0] 4 | def change 5 | create_table :schema_users do |t| 6 | ## Required 7 | t.string :provider, null: false, default: 'email' 8 | t.string :uid, null: false, default: '' 9 | 10 | ## Database authenticatable 11 | t.string :encrypted_password, null: false, default: '' 12 | 13 | ## Recoverable 14 | t.string :reset_password_token 15 | t.datetime :reset_password_sent_at 16 | t.boolean :allow_password_change, default: false 17 | 18 | ## Confirmable 19 | t.string :confirmation_token 20 | t.datetime :confirmed_at 21 | t.datetime :confirmation_sent_at 22 | 23 | # Trackable 24 | t.datetime :current_sign_in_at 25 | t.datetime :last_sign_in_at 26 | t.string :last_sign_in_ip 27 | t.string :current_sign_in_ip 28 | t.integer :sign_in_count 29 | 30 | ## User Info 31 | t.string :name 32 | t.string :email 33 | 34 | ## Tokens 35 | t.text :tokens 36 | 37 | t.timestamps 38 | end 39 | 40 | add_index :schema_users, :email, unique: true 41 | add_index :schema_users, [:uid, :provider], unique: true 42 | add_index :schema_users, :reset_password_token, unique: true 43 | add_index :schema_users, :confirmation_token, unique: true 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/api/v1/graphql_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class GraphqlController < ApplicationController 6 | include GraphqlDevise::SetUserByToken 7 | include ActionController::Cookies 8 | 9 | def graphql 10 | result = DummySchema.execute(params[:query], **execute_params(params)) 11 | 12 | render json: result unless performed? 13 | end 14 | 15 | def interpreter 16 | render json: InterpreterSchema.execute(params[:query], **execute_params(params)) 17 | end 18 | 19 | private 20 | 21 | def execute_params(item) 22 | { 23 | operation_name: item[:operationName], 24 | variables: ensure_hash(item[:variables]), 25 | context: gql_devise_context(SchemaUser, User) 26 | } 27 | end 28 | 29 | def ensure_hash(ambiguous_param) 30 | case ambiguous_param 31 | when String 32 | if ambiguous_param.present? 33 | ensure_hash(JSON.parse(ambiguous_param)) 34 | else 35 | {} 36 | end 37 | when Hash, ActionController::Parameters 38 | ambiguous_param 39 | when nil 40 | {} 41 | else 42 | raise ArgumentError, "Unexpected parameter: #{ambiguous_param}" 43 | end 44 | end 45 | 46 | def verify_authenticity_token 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module MountMethod 5 | module OperationPreparers 6 | class DefaultOperationPreparer 7 | def initialize(selected_operations:, custom_keys:, model:, preparer:) 8 | @selected_operations = selected_operations 9 | @custom_keys = custom_keys 10 | @model = model 11 | @preparer = preparer 12 | end 13 | 14 | def call 15 | mapping_name = ::GraphqlDevise.to_mapping_name(@model) 16 | 17 | @selected_operations.except(*@custom_keys).each_with_object({}) do |(action, operation_info), result| 18 | mapped_action = "#{mapping_name}_#{action}" 19 | operation = operation_info[:klass] 20 | options = operation_info.except(:klass, :deprecation_reason) 21 | 22 | result[mapped_action.to_sym] = [ 23 | OperationPreparers::GqlNameSetter.new(mapped_action), 24 | @preparer, 25 | OperationPreparers::ResourceKlassSetter.new(@model) 26 | ].reduce(child_class(operation)) do |prepared_operation, preparer| 27 | preparer.call(prepared_operation, **options) 28 | end 29 | end 30 | end 31 | 32 | private 33 | 34 | def child_class(operation) 35 | Class.new(operation) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | ENV['RAILS_ENV'] ||= 'test' 6 | GQL_DEVISE_ROOT = File.join(File.dirname(__FILE__), '../') 7 | 8 | require File.expand_path('dummy/config/environment.rb', __dir__) 9 | 10 | abort('The Rails environment is running in production mode!') if Rails.env.production? 11 | 12 | require 'rspec/rails' 13 | # Add additional requires below this line. Rails is not loaded until this point! 14 | require 'factory_bot' 15 | require 'faker' 16 | require 'generator_spec' 17 | 18 | # Load RSpec helpers. 19 | Dir[File.join(GQL_DEVISE_ROOT, 'spec/support/**/*.rb')].each { |f| require f } 20 | 21 | begin 22 | ActiveRecord::Migrator.migrations_paths = [ 23 | File.join(GQL_DEVISE_ROOT, 'spec/dummy/db/migrate'), 24 | File.join(GQL_DEVISE_ROOT, 'spec/db/migrate') 25 | ] 26 | ActiveRecord::Migration.maintain_test_schema! 27 | rescue ActiveRecord::PendingMigrationError => e 28 | puts e.to_s.strip 29 | exit 1 30 | end 31 | RSpec.configure do |config| 32 | config.use_transactional_fixtures = true 33 | 34 | config.infer_spec_type_from_file_location! 35 | 36 | config.filter_rails_from_backtrace! 37 | 38 | config.include(Requests::JsonHelpers, type: :request) 39 | config.include(Requests::AuthHelpers, type: :request) 40 | config.include(ActiveSupport::Testing::TimeHelpers) 41 | 42 | config.before(:suite) do 43 | ActionController::Base.allow_forgery_protection = true 44 | end 45 | config.before { ActionMailer::Base.deliveries.clear } 46 | end 47 | -------------------------------------------------------------------------------- /spec/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Puma can serve each request in a thread from an internal thread pool. 4 | # The `threads` method setting takes two numbers: a minimum and maximum. 5 | # Any libraries that use thread pools should be configured to match 6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 7 | # and maximum; this matches the default thread size of Active Record. 8 | # 9 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 10 | threads threads_count, threads_count 11 | 12 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 13 | # 14 | port ENV.fetch("PORT") { 3000 } 15 | 16 | # Specifies the `environment` that Puma will run in. 17 | # 18 | environment ENV.fetch("RAILS_ENV") { "development" } 19 | 20 | # Specifies the number of `workers` to boot in clustered mode. 21 | # Workers are forked webserver processes. If using threads and workers together 22 | # the concurrency of the application would be max `threads` * `workers`. 23 | # Workers do not work on JRuby or Windows (both of which do not support 24 | # processes). 25 | # 26 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 27 | 28 | # Use the `preload_app!` method when specifying a `workers` number. 29 | # This directive tells Puma to first boot the application and load code 30 | # before forking the application. This takes advantage of Copy On Write 31 | # process behavior so workers use less memory. 32 | # 33 | # preload_app! 34 | 35 | # Allow puma to be restarted by `rails restart` command. 36 | plugin :tmp_restart 37 | -------------------------------------------------------------------------------- /spec/services/mount_method/option_sanitizers/class_checker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OptionSanitizers::ClassChecker do 6 | describe '#call!' do 7 | subject(:clean_value) { described_class.new(expected_class).call!(value, key) } 8 | 9 | let(:key) { :any_option } 10 | let(:expected_class) { Numeric } 11 | 12 | context 'when no value is provided' do 13 | let(:value) { nil } 14 | 15 | it { is_expected.to eq(nil) } 16 | end 17 | 18 | context 'when provided value is not a class' do 19 | let(:value) { 'I\'m not a class' } 20 | 21 | it 'raises an error' do 22 | expect { clean_value }.to raise_error(GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has an invalid value. Class expected.") 23 | end 24 | end 25 | 26 | context 'when provided class is not of the expected type' do 27 | let(:value) { String } 28 | 29 | it 'raises an error' do 30 | expect { clean_value }.to raise_error(GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has an invalid value. #{expected_class} or descendants expected. Got String.") 31 | end 32 | end 33 | 34 | context 'when provided class is of the expected type' do 35 | let(:value) { Numeric } 36 | 37 | it { is_expected.to eq(value) } 38 | end 39 | 40 | context 'when provided class has the expected type as an acestor' do 41 | let(:value) { Float } 42 | 43 | it { is_expected.to eq(value) } 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/graphql_devise/mutations/resend_confirmation_with_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Mutations 5 | class ResendConfirmationWithToken < Base 6 | argument :email, String, required: true 7 | argument :confirm_url, String, required: true 8 | 9 | field :message, String, null: false 10 | 11 | def resolve(email:, confirm_url:) 12 | check_redirect_url_whitelist!(confirm_url) 13 | 14 | resource = find_confirmable_resource(email) 15 | 16 | if resource 17 | yield resource if block_given? 18 | 19 | if resource.confirmed? && !resource.pending_reconfirmation? 20 | raise_user_error(I18n.t('graphql_devise.confirmations.already_confirmed')) 21 | end 22 | 23 | resource.send_confirmation_instructions( 24 | redirect_url: confirm_url, 25 | template_path: ['graphql_devise/mailer'] 26 | ) 27 | 28 | { message: I18n.t('graphql_devise.confirmations.send_instructions', email: email) } 29 | else 30 | raise_user_error(I18n.t('graphql_devise.confirmations.user_not_found', email: email)) 31 | end 32 | end 33 | 34 | private 35 | 36 | def find_confirmable_resource(email) 37 | email_insensitive = get_case_insensitive_field(:email, email) 38 | resource = find_resource(:unconfirmed_email, email_insensitive) if resource_class.reconfirmable 39 | resource ||= find_resource(:email, email_insensitive) 40 | resource 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/services/mount_method/option_validators/supported_operations_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OptionValidators::SupportedOperationsValidator do 6 | describe '#validate!' do 7 | subject { -> { described_class.new(provided_operations: provided_operations, supported_operations: supported_operations, key: key).validate! } } 8 | 9 | let(:supported_operations) { [:operation1, :operation2, :operation3] } 10 | let(:key) { :only } 11 | 12 | context 'when custom operations are all supported' do 13 | let(:provided_operations) { [:operation2, :operation3] } 14 | 15 | it { is_expected.not_to raise_error } 16 | end 17 | 18 | context 'when no operations are provided' do 19 | let(:provided_operations) { [] } 20 | 21 | it { is_expected.not_to raise_error } 22 | end 23 | 24 | context 'when default_operations are empty' do 25 | let(:supported_operations) { [] } 26 | let(:provided_operations) { [:invalid] } 27 | 28 | it { is_expected.to raise_error(GraphqlDevise::InvalidMountOptionsError, 'only option contains unsupported operations: "invalid". Check for typos.') } 29 | end 30 | 31 | context 'when not all custom operations are supported' do 32 | let(:provided_operations) { [:operation2, :operation3, :unsupported] } 33 | 34 | it { is_expected.to raise_error(GraphqlDevise::InvalidMountOptionsError, 'only option contains unsupported operations: "unsupported". Check for typos.') } 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/graphql_devise/field_auth_tracer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module FieldAuthTracer 5 | def initialize(authenticate_default:, public_introspection:, unauthenticated_proc:, **_rest) 6 | @authenticate_default = authenticate_default 7 | @public_introspection = public_introspection 8 | @unauthenticated_proc = unauthenticated_proc 9 | 10 | super 11 | end 12 | 13 | def execute_field(field:, query:, ast_node:, arguments:, object:) 14 | # Authenticate only root level queries 15 | return super unless query.context.current_path.count == 1 16 | 17 | auth_required = authenticate_option(field) 18 | 19 | if auth_required && !(public_introspection && introspection_field?(field.name)) 20 | raise_on_missing_resource(query.context, field, auth_required) 21 | end 22 | 23 | super 24 | end 25 | 26 | private 27 | 28 | attr_reader :public_introspection 29 | 30 | def authenticate_option(field) 31 | auth_required = field.try(:authenticate) 32 | 33 | auth_required.nil? ? @authenticate_default : auth_required 34 | end 35 | 36 | def introspection_field?(field_name) 37 | SchemaPlugin::INTROSPECTION_FIELDS.include?(field_name.downcase) 38 | end 39 | 40 | def raise_on_missing_resource(context, field, auth_required) 41 | @unauthenticated_proc.call(field.name) if context[:current_resource].blank? 42 | 43 | if auth_required.respond_to?(:call) && !auth_required.call(context[:current_resource]) 44 | @unauthenticated_proc.call(field.name) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/services/mount_method/options_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OptionsValidator do 6 | describe '#validate!' do 7 | subject { -> { described_class.new([validator1, validator2]).validate! } } 8 | 9 | let(:validator1) { double(:validator1, 'validate!': nil) } 10 | let(:validator2) { double(:validator2, 'validate!': nil) } 11 | 12 | context 'when first validator fails' do 13 | before { allow(validator1).to receive(:validate!).and_raise(GraphqlDevise::InvalidMountOptionsError, 'validator1 error') } 14 | 15 | context 'when second validator fails' do 16 | before { allow(validator2).to receive(:validate!).and_raise(GraphqlDevise::InvalidMountOptionsError, 'validator2 error') } 17 | 18 | it { is_expected.to raise_error(GraphqlDevise::InvalidMountOptionsError, 'validator1 error') } 19 | end 20 | 21 | context 'when second validator does not fail' do 22 | it { is_expected.to raise_error(GraphqlDevise::InvalidMountOptionsError, 'validator1 error') } 23 | end 24 | end 25 | 26 | context 'when first validator does not fail' do 27 | context 'when second validator fails' do 28 | before { allow(validator2).to receive(:validate!).and_raise(GraphqlDevise::InvalidMountOptionsError, 'validator2 error') } 29 | 30 | it { is_expected.to raise_error(GraphqlDevise::InvalidMountOptionsError, 'validator2 error') } 31 | end 32 | 33 | context 'when second validator does not fail' do 34 | it { is_expected.not_to raise_error } 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/graphql_devise/mutations/update_password_with_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Mutations 5 | class UpdatePasswordWithToken < Base 6 | argument :password, String, required: true 7 | argument :password_confirmation, String, required: true 8 | argument :reset_password_token, String, required: true 9 | 10 | field :credentials, 11 | Types::CredentialType, 12 | null: true, 13 | description: 'Authentication credentials. Resource must be signed_in for credentials to be returned.' 14 | 15 | def resolve(reset_password_token:, **attrs) 16 | raise_user_error(I18n.t('graphql_devise.passwords.password_recovery_disabled')) unless recoverable_enabled? 17 | 18 | resource = resource_class.with_reset_password_token(reset_password_token) 19 | raise_user_error(I18n.t('graphql_devise.passwords.reset_token_not_found')) if resource.blank? 20 | raise_user_error(I18n.t('graphql_devise.passwords.reset_token_expired')) unless resource.reset_password_period_valid? 21 | 22 | if resource.update(attrs) 23 | yield resource if block_given? 24 | 25 | response_payload = { authenticatable: resource } 26 | response_payload[:credentials] = generate_auth_headers(resource) if controller.signed_in?(resource_name) 27 | 28 | response_payload 29 | else 30 | raise_user_error_list( 31 | I18n.t('graphql_devise.passwords.update_password_error'), 32 | resource: resource 33 | ) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::CustomOperationPreparer do 6 | describe '#call' do 7 | subject(:prepared) { described_class.new(selected_keys: selected_keys, custom_operations: operations, model: model).call } 8 | 9 | let(:login_operation) { double(:confirm_operation, graphql_name: nil) } 10 | let(:logout_operation) { double(:sign_up_operation, graphql_name: nil) } 11 | let(:model) { User } 12 | let(:operations) { { login: login_operation, logout: logout_operation, invalid: double(:invalid) } } 13 | let(:selected_keys) { [:login, :logout, :sign_up, :confirm] } 14 | 15 | it 'returns only those operations with no custom operation provided' do 16 | expect(prepared.keys).to contain_exactly(:user_login, :user_logout) 17 | end 18 | 19 | it 'prepares custom operations' do 20 | expect(login_operation).to receive(:graphql_name).with('UserLogin') 21 | expect(logout_operation).to receive(:graphql_name).with('UserLogout') 22 | 23 | prepared 24 | 25 | expect(login_operation.instance_variable_get(:@resource_klass)).to eq(User) 26 | expect(logout_operation.instance_variable_get(:@resource_klass)).to eq(User) 27 | end 28 | 29 | context 'when no selected keys are provided' do 30 | let(:selected_keys) { [] } 31 | 32 | it 'returns no operations' do 33 | expect(prepared).to eq({}) 34 | end 35 | end 36 | 37 | context 'when no custom operations are provided' do 38 | let(:operations) { {} } 39 | 40 | it 'returns no operations' do 41 | expect(prepared).to eq({}) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20190815114303_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateUsers < ActiveRecord::Migration[5.2] 4 | def change 5 | create_table :users do |t| 6 | ## Required 7 | t.string :provider, null: false, default: 'email' 8 | t.string :uid, null: false, default: '' 9 | 10 | ## Database authenticatable 11 | t.string :encrypted_password, null: false, default: '' 12 | 13 | ## Recoverable 14 | t.string :reset_password_token 15 | t.datetime :reset_password_sent_at 16 | t.boolean :allow_password_change, default: false 17 | 18 | ## Rememberable 19 | t.datetime :remember_created_at 20 | 21 | ## Confirmable 22 | t.string :confirmation_token 23 | t.datetime :confirmed_at 24 | t.datetime :confirmation_sent_at 25 | t.string :unconfirmed_email # Only if using reconfirmable 26 | 27 | ## Lockable 28 | t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 29 | t.string :unlock_token # Only if unlock strategy is :email or :both 30 | t.datetime :locked_at 31 | 32 | # Trackable 33 | t.datetime :current_sign_in_at 34 | t.datetime :last_sign_in_at 35 | t.string :last_sign_in_ip 36 | t.string :current_sign_in_ip 37 | t.integer :sign_in_count 38 | 39 | ## User Info 40 | t.string :name 41 | t.string :email 42 | 43 | ## Tokens 44 | t.text :tokens 45 | 46 | t.timestamps 47 | end 48 | 49 | add_index :users, :email, unique: true 50 | add_index :users, [:uid, :provider], unique: true 51 | add_index :users, :reset_password_token, unique: true 52 | add_index :users, :confirmation_token, unique: true 53 | add_index :users, :unlock_token, unique: true 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded on 7 | # every request. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Enable/disable caching. By default caching is disabled. 15 | # Run rails dev:cache to toggle caching. 16 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Show full error reports and disable caching. 30 | config.consider_all_requests_local = true 31 | config.action_controller.perform_caching = false 32 | 33 | # Raise exceptions instead of rendering exception templates. 34 | config.action_dispatch.show_exceptions = false 35 | 36 | # Disable request forgery protection in test environment. 37 | config.action_controller.allow_forgery_protection = false 38 | 39 | # Tell Action Mailer not to deliver emails to the real world. 40 | # The :test delivery method accumulates sent emails in the 41 | # ActionMailer::Base.deliveries array. 42 | config.action_mailer.delivery_method = :test 43 | config.action_mailer.default_url_options = { host: 'localhost' } 44 | 45 | # Print deprecation notices to the stderr. 46 | config.active_support.deprecation = :stderr 47 | end 48 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # The test environment is used exclusively to run your application's 7 | # test suite. You never need to work with it otherwise. Remember that 8 | # your test database is "scratch space" for the test suite and is wiped 9 | # and recreated between test runs. Don't rely on the data there! 10 | config.cache_classes = true 11 | 12 | # Do not eager load code on boot. This avoids loading your whole application 13 | # just for the purpose of running a single test. If you are using a tool that 14 | # preloads Rails for running tests, you may have to set it to true. 15 | config.eager_load = ENV['EAGER_LOAD'].present? 16 | 17 | # Configure public file server for tests with Cache-Control for performance. 18 | config.public_file_server.enabled = true 19 | config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Tell Action Mailer not to deliver emails to the real world. 32 | # The :test delivery method accumulates sent emails in the 33 | # ActionMailer::Base.deliveries array. 34 | config.action_mailer.delivery_method = :test 35 | config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | config.action_view.raise_on_missing_translations = true if Rails::VERSION::MAJOR < 7 42 | end 43 | -------------------------------------------------------------------------------- /spec/requests/mutations/logout_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'Logout Requests' do 6 | include_context 'with graphql query request' 7 | 8 | let(:user) { create(:user, :confirmed) } 9 | let(:query) do 10 | <<-GRAPHQL 11 | mutation { 12 | userLogout { 13 | authenticatable { email } 14 | } 15 | } 16 | GRAPHQL 17 | end 18 | 19 | before { post_request } 20 | 21 | context 'when user is logged in' do 22 | let(:headers) { user.create_new_auth_token } 23 | 24 | it 'logs out the user' do 25 | expect(response).not_to include_auth_headers 26 | expect(user.reload.tokens.keys).to be_empty 27 | expect(json_response[:data][:userLogout]).to match( 28 | authenticatable: { email: user.email } 29 | ) 30 | expect(json_response[:errors]).to be_nil 31 | end 32 | end 33 | 34 | context 'when user is not logged in' do 35 | it 'returns an error' do 36 | expect(response).not_to include_auth_headers 37 | expect(user.reload.tokens.keys).to be_empty 38 | expect(json_response[:data][:userLogout]).to be_nil 39 | expect(json_response[:errors]).to contain_exactly( 40 | hash_including(message: 'User was not found or was not logged in.', extensions: { code: 'USER_ERROR' }) 41 | ) 42 | end 43 | end 44 | 45 | context 'when using the admin model' do 46 | let(:query) do 47 | <<-GRAPHQL 48 | mutation { 49 | adminLogout { 50 | authenticatable { email } 51 | } 52 | } 53 | GRAPHQL 54 | end 55 | let(:admin) { create(:admin, :confirmed) } 56 | let(:headers) { admin.create_new_auth_token } 57 | 58 | it 'logs out the admin' do 59 | expect(response).not_to include_auth_headers 60 | expect(admin.reload.tokens.keys).to be_empty 61 | expect(json_response[:data][:adminLogout]).to match( 62 | authenticatable: { email: admin.email } 63 | ) 64 | expect(json_response[:errors]).to be_nil 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/requests/mutations/additional_mutations_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'Additional Mutations' do 6 | include_context 'with graphql query request' 7 | 8 | let(:name) { Faker::Name.name } 9 | let(:password) { Faker::Internet.password } 10 | let(:password_confirmation) { password } 11 | let(:email) { Faker::Internet.email } 12 | 13 | context 'when using the user model' do 14 | let(:query) do 15 | <<-GRAPHQL 16 | mutation { 17 | registerConfirmedUser( 18 | email: "#{email}", 19 | name: "#{name}", 20 | password: "#{password}", 21 | passwordConfirmation: "#{password_confirmation}" 22 | ) { 23 | user { 24 | email 25 | name 26 | } 27 | } 28 | } 29 | GRAPHQL 30 | end 31 | 32 | context 'when params are correct' do 33 | it 'creates a new resource that is already confirmed' do 34 | expect { post_request }.to( 35 | change(User, :count).by(1) 36 | .and(not_change(ActionMailer::Base.deliveries, :count)) 37 | ) 38 | 39 | user = User.last 40 | 41 | expect(user).to be_confirmed 42 | expect(json_response[:data][:registerConfirmedUser]).to include( 43 | user: { 44 | email: email, 45 | name: name 46 | } 47 | ) 48 | end 49 | end 50 | 51 | context 'when params are incorrect' do 52 | let(:password_confirmation) { 'not the same' } 53 | 54 | it 'returns descriptive errors' do 55 | expect { post_request }.to not_change(User, :count) 56 | 57 | expect(json_response[:errors]).to contain_exactly( 58 | hash_including( 59 | message: 'Custom registration failed', 60 | extensions: { code: 'USER_ERROR', detailed_errors: ["Password confirmation doesn't match Password"] } 61 | ) 62 | ) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/graphql_devise.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails' 4 | require 'rails/generators' 5 | require 'graphql' 6 | require 'devise_token_auth' 7 | require 'zeitwerk' 8 | 9 | if Gem::Version.new(GraphQL::VERSION) < Gem::Version.new('2.0') 10 | GraphQL::Field.accepts_definitions(authenticate: GraphQL::Define.assign_metadata_key(:authenticate)) 11 | GraphQL::Schema::Field.accepts_definition(:authenticate) 12 | end 13 | 14 | loader = Zeitwerk::Loader.for_gem 15 | 16 | loader.collapse("#{__dir__}/graphql_devise/concerns") 17 | loader.collapse("#{__dir__}/graphql_devise/errors") 18 | loader.ignore("#{__dir__}/generators") 19 | 20 | loader.inflector.inflect('error_codes' => 'ERROR_CODES') 21 | loader.inflector.inflect('supported_options' => 'SUPPORTED_OPTIONS') 22 | 23 | loader.setup 24 | 25 | module GraphqlDevise 26 | class Error < StandardError; end 27 | 28 | class InvalidMountOptionsError < ::GraphqlDevise::Error; end 29 | 30 | @schema_loaded = false 31 | @mounted_resources = [] 32 | 33 | def self.schema_loaded? 34 | @schema_loaded 35 | end 36 | 37 | def self.load_schema 38 | @schema_loaded = true 39 | end 40 | 41 | def self.resource_mounted?(model) 42 | @mounted_resources.include?(model) 43 | end 44 | 45 | def self.mount_resource(model) 46 | @mounted_resources << model 47 | end 48 | 49 | def self.add_mapping(mapping_name, resource) 50 | return if Devise.mappings.key?(mapping_name.to_sym) 51 | 52 | Devise.add_mapping( 53 | mapping_name.to_s.pluralize.to_sym, 54 | module: :devise, class_name: resource.to_s 55 | ) 56 | end 57 | 58 | def self.to_mapping_name(resource) 59 | resource.to_s.underscore.tr('/', '_') 60 | end 61 | 62 | def self.configure_warden_serializer_for_model(model) 63 | Devise.warden_config.serialize_into_session(to_mapping_name(model)) do |record| 64 | model.serialize_into_session(record) 65 | end 66 | 67 | Devise.warden_config.serialize_from_session(to_mapping_name(model)) do |args| 68 | model.serialize_from_session(*args) 69 | end 70 | end 71 | end 72 | 73 | ActionDispatch::Routing::Mapper.include(GraphqlDevise::RouteMounter) 74 | 75 | require 'graphql_devise/engine' 76 | -------------------------------------------------------------------------------- /lib/graphql_devise/mutations/login.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Mutations 5 | class Login < Base 6 | argument :email, String, required: true 7 | argument :password, String, required: true 8 | 9 | field :credentials, Types::CredentialType, null: false 10 | 11 | def resolve(email:, password:) 12 | resource = find_resource( 13 | :email, 14 | get_case_insensitive_field(:email, email) 15 | ) 16 | 17 | if resource && active_for_authentication?(resource) 18 | if invalid_for_authentication?(resource, password) 19 | raise_user_error(I18n.t('graphql_devise.sessions.bad_credentials')) 20 | end 21 | 22 | new_headers = generate_auth_headers(resource) 23 | controller.sign_in(:user, resource, store: false, bypass: false) 24 | 25 | yield resource if block_given? 26 | 27 | context[:current_resource] = resource if context[:current_resource].nil? 28 | 29 | { authenticatable: resource, credentials: new_headers } 30 | elsif resource && !active_for_authentication?(resource) 31 | if locked?(resource) 32 | raise_user_error(I18n.t('graphql_devise.mailer.unlock_instructions.account_lock_msg')) 33 | else 34 | raise_user_error(I18n.t('graphql_devise.sessions.not_confirmed', email: resource.email)) 35 | end 36 | else 37 | raise_user_error(I18n.t('graphql_devise.sessions.bad_credentials')) 38 | end 39 | end 40 | 41 | private 42 | 43 | def invalid_for_authentication?(resource, password) 44 | valid_password = resource.valid_password?(password) 45 | 46 | (resource.respond_to?(:valid_for_authentication?) && !resource.valid_for_authentication? { valid_password }) || 47 | !valid_password 48 | end 49 | 50 | def active_for_authentication?(resource) 51 | !resource.respond_to?(:active_for_authentication?) || resource.active_for_authentication? 52 | end 53 | 54 | def locked?(resource) 55 | resource.respond_to?(:locked_at) && resource.locked_at 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/graphql_devise/mutations/register.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Mutations 5 | class Register < Base 6 | argument :email, String, required: true 7 | argument :password, String, required: true 8 | argument :password_confirmation, String, required: true 9 | argument :confirm_url, String, required: false 10 | 11 | field :credentials, 12 | Types::CredentialType, 13 | null: true, 14 | description: 'Authentication credentials. Null if after signUp resource is not active for authentication (e.g. Email confirmation required).' 15 | 16 | def resolve(confirm_url: nil, **attrs) 17 | resource = build_resource(attrs.merge(provider: provider)) 18 | raise_user_error(I18n.t('graphql_devise.resource_build_failed')) if resource.blank? 19 | 20 | redirect_url = confirm_url || DeviseTokenAuth.default_confirm_success_url 21 | if confirmable_enabled? && redirect_url.blank? 22 | raise_user_error(I18n.t('graphql_devise.registrations.missing_confirm_redirect_url')) 23 | end 24 | 25 | check_redirect_url_whitelist!(redirect_url) 26 | 27 | resource.skip_confirmation_notification! if resource.respond_to?(:skip_confirmation_notification!) 28 | 29 | if resource.save 30 | yield resource if block_given? 31 | 32 | unless resource.confirmed? 33 | resource.send_confirmation_instructions( 34 | redirect_url: redirect_url, 35 | template_path: ['graphql_devise/mailer'] 36 | ) 37 | end 38 | 39 | response_payload = { authenticatable: resource } 40 | 41 | response_payload[:credentials] = generate_auth_headers(resource) if resource.active_for_authentication? 42 | 43 | response_payload 44 | else 45 | resource.try(:clean_up_passwords) 46 | raise_user_error_list( 47 | I18n.t('graphql_devise.registration_failed'), 48 | resource: resource 49 | ) 50 | end 51 | end 52 | 53 | private 54 | 55 | def build_resource(attrs) 56 | resource_class.new(attrs) 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/graphql_devise/model/with_email_updater.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module Model 5 | class WithEmailUpdater 6 | def initialize(resource, attributes) 7 | @attributes = attributes.with_indifferent_access 8 | @resource = resource 9 | end 10 | 11 | def call 12 | resource_attributes = @attributes.except(:confirmation_url) 13 | return @resource.update(resource_attributes) unless requires_reconfirmation?(resource_attributes) 14 | 15 | @resource.assign_attributes(resource_attributes) 16 | 17 | if @resource.email == email_in_database 18 | @resource.save 19 | elsif required_reconfirm_attributes? 20 | return false unless @resource.valid? 21 | 22 | store_unconfirmed_email 23 | saved = @resource.save 24 | send_confirmation_instructions(saved) 25 | 26 | saved 27 | else 28 | raise( 29 | ::GraphqlDevise::Error, 30 | 'Method `update_with_email` requires attribute `confirmation_url` for email reconfirmation to work' 31 | ) 32 | end 33 | end 34 | 35 | private 36 | 37 | def required_reconfirm_attributes? 38 | [@attributes[:confirmation_url], DeviseTokenAuth.default_confirm_success_url].any?(&:present?) 39 | end 40 | 41 | def requires_reconfirmation?(resource_attributes) 42 | resource_attributes.key?(:email) && 43 | @resource.devise_modules.include?(:confirmable) && 44 | @resource.respond_to?(:unconfirmed_email=) 45 | end 46 | 47 | def store_unconfirmed_email 48 | @resource.unconfirmed_email = @resource.email 49 | @resource.confirmation_token = nil 50 | @resource.email = email_in_database 51 | @resource.send(:generate_confirmation_token) 52 | end 53 | 54 | def email_in_database 55 | @resource.email_in_database 56 | end 57 | 58 | def confirmation_method_params 59 | { redirect_url: @attributes[:confirmation_url] || DeviseTokenAuth.default_confirm_success_url } 60 | end 61 | 62 | def send_confirmation_instructions(saved) 63 | return unless saved 64 | 65 | @resource.send_confirmation_instructions( 66 | confirmation_method_params.merge(template_path: ['graphql_devise/mailer']) 67 | ) 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /graphql_devise.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'graphql_devise/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'graphql_devise' 7 | spec.version = GraphqlDevise::VERSION 8 | spec.authors = ['Mario Celi', 'David Revelo'] 9 | spec.email = ['mcelicalderon@gmail.com', 'david.revelo.uio@gmail.com'] 10 | 11 | spec.summary = 'GraphQL queries and mutations on top of devise_token_auth' 12 | spec.description = 'GraphQL queries and mutations on top of devise_token_auth' 13 | spec.homepage = 'https://github.com/graphql-devise/graphql_devise' 14 | spec.license = 'MIT' 15 | 16 | spec.metadata['homepage_uri'] = spec.homepage 17 | spec.metadata['source_code_uri'] = 'https://github.com/graphql-devise/graphql_devise' 18 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 19 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|gemfiles)/}) } 20 | end 21 | spec.bindir = 'exe' 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.require_paths = ['lib'] 24 | spec.test_files = Dir.chdir(File.expand_path(__dir__)) do 25 | `git ls-files -z`.split("\x0").select { |f| f.match(%r{^spec/}) } 26 | end 27 | 28 | spec.required_ruby_version = '>= 2.7.0' 29 | 30 | spec.add_dependency 'devise_token_auth', '>= 0.1.43', '< 2.0' 31 | spec.add_dependency 'graphql', '>= 1.8', '< 2.6' 32 | spec.add_dependency 'rails', '>= 6.0', '< 8.1' 33 | spec.add_dependency 'zeitwerk' 34 | 35 | spec.add_development_dependency 'appraisal' 36 | spec.add_development_dependency 'coveralls_reborn' 37 | spec.add_development_dependency 'factory_bot' 38 | spec.add_development_dependency 'faker' 39 | spec.add_development_dependency 'generator_spec' 40 | spec.add_development_dependency 'github_changelog_generator' 41 | spec.add_development_dependency 'pry', '>= 0.14.2' 42 | spec.add_development_dependency 'pry-byebug' 43 | spec.add_development_dependency 'rake', '>= 12.3.3' 44 | spec.add_development_dependency 'rspec-rails' 45 | spec.add_development_dependency 'rubocop', '< 0.82.0' 46 | spec.add_development_dependency 'rubocop-performance', '< 1.6.0' 47 | spec.add_development_dependency 'rubocop-rails', '< 2.6.0' 48 | spec.add_development_dependency 'rubocop-rspec', '< 1.39.0' 49 | spec.add_development_dependency 'sqlite3' 50 | end 51 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rspec 3 | - rubocop-performance 4 | - rubocop-rails 5 | 6 | AllCops: 7 | TargetRubyVersion: 2.7 8 | DisplayCopNames: true 9 | Exclude: 10 | - bin/**/* 11 | - db/**/* 12 | - vendor/**/* 13 | - tmp/**/* 14 | 15 | Rails/HttpPositionalArguments: 16 | Enabled: false 17 | 18 | Rails/HasAndBelongsToMany: 19 | Enabled: false 20 | 21 | Rails/HasManyOrHasOneDependent: 22 | Enabled: false 23 | 24 | Rails/OutputSafety: 25 | Enabled: false 26 | 27 | RSpec/MultipleExpectations: 28 | Enabled: false 29 | 30 | RSpec/ExampleLength: 31 | Enabled: false 32 | 33 | RSpec/RepeatedDescription: 34 | Enabled: false 35 | 36 | RSpec/MessageSpies: 37 | EnforcedStyle: receive 38 | 39 | Style/AndOr: 40 | Enabled: false 41 | 42 | Style/Documentation: 43 | Enabled: false 44 | 45 | Style/MethodCalledOnDoEndBlock: 46 | Enabled: true 47 | 48 | Style/CollectionMethods: 49 | Enabled: true 50 | 51 | Style/SymbolArray: 52 | Enabled: false 53 | 54 | Naming/AccessorMethodName: 55 | Enabled: false 56 | 57 | Style/MethodCalledOnDoEndBlock: 58 | Enabled: false 59 | 60 | Naming/VariableNumber: 61 | EnforcedColonStyle: normalcase 62 | 63 | Style/StringLiterals: 64 | ConsistentQuotesInMultiline: true 65 | 66 | Style/ClassAndModuleChildren: 67 | Enabled: false 68 | 69 | Style/GuardClause: 70 | Enabled: false 71 | 72 | Style/EmptyMethod: 73 | EnforcedStyle: expanded 74 | SupportedStyles: 75 | - compact 76 | - expanded 77 | 78 | Style/FrozenStringLiteralComment: 79 | Enabled: false 80 | 81 | Style/StringMethods: 82 | Enabled: true 83 | 84 | Layout/LineLength: 85 | Max: 120 86 | 87 | Metrics/MethodLength: 88 | Max: 15 89 | 90 | Metrics/BlockLength: 91 | Enabled: false 92 | 93 | Layout/HashAlignment: 94 | EnforcedColonStyle: table 95 | 96 | Layout/ParameterAlignment: 97 | EnforcedStyle: with_fixed_indentation 98 | SupportedStyles: 99 | - with_first_parameter 100 | - with_fixed_indentation 101 | 102 | Layout/EndAlignment: 103 | EnforcedStyleAlignWith: variable 104 | SupportedStylesAlignWith: 105 | - keyword 106 | - variable 107 | 108 | Lint/MissingCopEnableDirective: 109 | Enabled: false 110 | 111 | RSpec/NestedGroups: 112 | Max: 7 113 | 114 | RSpec/ContextWording: 115 | Prefixes: 116 | - when 117 | - with 118 | - without 119 | - and 120 | -------------------------------------------------------------------------------- /spec/requests/graphql_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe GraphqlDevise::GraphqlController do 6 | let(:password) { 'password123' } 7 | let(:user) { create(:user, :confirmed, password: password) } 8 | let(:params) { { query: query, variables: variables } } 9 | 10 | context 'when variables are a string' do 11 | let(:variables) { "{\"email\": \"#{user.email}\"}" } 12 | let(:query) { "mutation($email: String!) { userLogin(email: $email, password: \"#{password}\") { user { email name signInCount } } }" } 13 | 14 | it 'parses the string variables' do 15 | post_request('/api/v1/graphql_auth') 16 | 17 | expect(json_response).to match( 18 | data: { userLogin: { user: { email: user.email, name: user.name, signInCount: 1 } } } 19 | ) 20 | end 21 | 22 | context 'when variables is an empty string' do 23 | let(:variables) { '' } 24 | let(:query) { "mutation { userLogin(email: \"#{user.email}\", password: \"#{password}\") { user { email name signInCount } } }" } 25 | 26 | it 'returns an empty hash as variables' do 27 | post_request('/api/v1/graphql_auth') 28 | 29 | expect(json_response).to match( 30 | data: { userLogin: { user: { email: user.email, name: user.name, signInCount: 1 } } } 31 | ) 32 | end 33 | end 34 | end 35 | 36 | context 'when multiplexing queries' do 37 | let(:params) do 38 | { 39 | _json: [ 40 | { query: "mutation { userLogin(email: \"#{user.email}\", password: \"#{password}\") { user { email name signInCount } } }" }, 41 | { query: "mutation { userLogin(email: \"#{user.email}\", password: \"wrong password\") { user { email name signInCount } } }" } 42 | ] 43 | } 44 | end 45 | 46 | it 'executes multiple queries in the same request' do 47 | post_request('/api/v1/graphql_auth') 48 | 49 | expect(json_response).to match( 50 | [ 51 | { data: { userLogin: { user: { email: user.email, name: user.name, signInCount: 1 } } } }, 52 | { 53 | data: { userLogin: nil }, 54 | errors: [ 55 | hash_including( 56 | message: 'Invalid login credentials. Please try again.', extensions: { code: 'USER_ERROR' } 57 | ) 58 | ] 59 | } 60 | ] 61 | ) 62 | end 63 | end 64 | 65 | def post_request(path) 66 | post(path, params: params) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in the gem 4 | labels: 'issue: bug, needs triage' 5 | --- 6 | 7 | 11 | 12 | ### Describe the bug 13 | 14 | (Describe the problem you are experiencing here.) 15 | 16 | ### Environment 17 | 18 | 21 | 22 | (paste the link(s) to the Gemfile and Gemfile.lock files.) 23 | 24 | ### Steps to reproduce 25 | 26 | 30 | 31 | (Write your steps here:) 32 | 33 | 1. 34 | 2. 35 | 3. 36 | 37 | ### Expected behavior 38 | 39 | 44 | 45 | (Write what you thought would happen.) 46 | 47 | ### Actual behavior 48 | 49 | 54 | 55 | (Write what happened. Please add stacktraces or http responses!) 56 | 57 | ### Reproducible demo 58 | 59 | 69 | 70 | (Paste the link to an example project and exact instructions to reproduce the issue.) 71 | 72 | 82 | -------------------------------------------------------------------------------- /spec/services/mount_method/option_validators/provided_operations_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OptionValidators::ProvidedOperationsValidator do 6 | describe '#validate!' do 7 | subject { -> { described_class.new(options: provided_operations, supported_operations: supported_operations).validate! } } 8 | 9 | let(:supported_operations) { { operation1: 'irrelevant', operation2: 'irrelevant', operation3: 'irrelevant' } } 10 | 11 | context 'when skip option is provided' do 12 | let(:provided_operations) { double(:clean_options, only: [], skip: skipped, operations: {}) } 13 | 14 | context 'when all skipped are supported' do 15 | let(:skipped) { [:operation2, :operation3] } 16 | 17 | it { is_expected.not_to raise_error } 18 | end 19 | 20 | context 'when skipped contains unsupported operations' do 21 | let(:skipped) { [:operation2, :operation3, :invalid] } 22 | 23 | it { is_expected.to raise_error(GraphqlDevise::InvalidMountOptionsError, 'skip option contains unsupported operations: "invalid". Check for typos.') } 24 | end 25 | end 26 | 27 | context 'when only option is provided' do 28 | let(:provided_operations) { double(:clean_options, skip: [], only: only, operations: {}) } 29 | 30 | context 'when all only are supported' do 31 | let(:only) { [:operation2, :operation3] } 32 | 33 | it { is_expected.not_to raise_error } 34 | end 35 | 36 | context 'when only contains unsupported operations' do 37 | let(:only) { [:operation2, :operation3, :invalid] } 38 | 39 | it { is_expected.to raise_error(GraphqlDevise::InvalidMountOptionsError, 'only option contains unsupported operations: "invalid". Check for typos.') } 40 | end 41 | end 42 | 43 | context 'when operations option is provided' do 44 | let(:provided_operations) { double(:clean_options, only: [], skip: [], operations: operations) } 45 | 46 | context 'when all operations are supported' do 47 | let(:operations) { { operation2: 'irrelevant', operation3: 'irrelevant' } } 48 | 49 | it { is_expected.not_to raise_error } 50 | end 51 | 52 | context 'when operations contains unsupported operations' do 53 | let(:operations) { { operation2: 'irrelevant', operation3: 'irrelevant', invalid: 'invalid' } } 54 | 55 | it { is_expected.to raise_error(GraphqlDevise::InvalidMountOptionsError, 'operations option contains unsupported operations: "invalid". Check for typos.') } 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | graphql_devise: 3 | redirect_url_not_allowed: "Redirect to '%{redirect_url}' not allowed." 4 | registration_failed: "User couldn't be registered" 5 | resource_build_failed: "Resource couldn't be built, execution stopped." 6 | not_authenticated: "User is not logged in." 7 | user_not_found: "User was not found or was not logged in." 8 | invalid_resource: "Errors present in the resource." 9 | registrations: 10 | missing_confirm_redirect_url: "Missing 'confirm_success_url' parameter. Required when confirmable module is enabled." 11 | passwords: 12 | password_recovery_disabled: "You must enable password recovery for this model." 13 | update_password_error: "Unable to update user password" 14 | missing_passwords: "You must fill out the fields labeled 'Password' and 'Password confirmation'." 15 | password_not_required: "This account does not require a password. Sign in using your '%{provider}' account instead." 16 | reset_token_not_found: "No user found for the specified reset token." 17 | reset_token_expired: "Reset password token is no longer valid." 18 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 19 | sessions: 20 | bad_credentials: "Invalid login credentials. Please try again." 21 | 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" 22 | confirmations: 23 | already_confirmed: "Email was already confirmed, please try signing in" 24 | invalid_token: "Invalid confirmation token. Please try again" 25 | user_not_found: "Unable to find user with email '%{email}'." 26 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 27 | mailer: 28 | confirmation_instructions: 29 | confirm_link_msg: "You can confirm your account email through the link below:" 30 | confirm_account_link: "Confirm my account" 31 | reset_password_instructions: 32 | request_reset_link_msg: "Someone has requested a link to change your password. You can do this through the link below." 33 | password_change_link: "Change my password" 34 | ignore_mail_msg: "If you didn't request this, please ignore this email." 35 | no_changes_msg: "Your password won't change until you access the link above and create a new one." 36 | unlock_instructions: 37 | account_lock_msg: "Your account has been locked due to an excessive number of unsuccessful sign in attempts." 38 | hello: "hello" 39 | welcome: "welcome" 40 | -------------------------------------------------------------------------------- /config/locales/pt-BR.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | graphql_devise: 3 | redirect_url_not_allowed: "Redirecionamento para '%{redirect_url}' não permitido." 4 | registration_failed: "Usuário não pôde ser registrado" 5 | resource_build_failed: "O recurso não pôde ser construído, execução interrompida." 6 | not_authenticated: "Usuário não logado." 7 | user_not_found: "Usuário não encontrado ou não logado." 8 | invalid_resource: "Erros presentes no recurso." 9 | registrations: 10 | missing_confirm_redirect_url: "Parâmetro 'confirm_success_url' ausente. Requerido quando o módulo 'confirmable' está habilitado." 11 | passwords: 12 | password_recovery_disabled: "Você deve habilitar a recuperação de senha para este model." 13 | update_password_error: "Impossível atualizar a senha do usuário" 14 | missing_passwords: "Você deve preencher os campos denominados 'Senha' e 'Confirmação de senha'." 15 | password_not_required: "Esta conta não requer senha. Faça login usando sua conta '%{provider}'." 16 | reset_token_not_found: "Nenhum usuário encontrado para o token de redefinição especificado." 17 | reset_token_expired: "O token de redefinição de senha não é mais válido." 18 | send_instructions: "Você receberá um e-mail com instruções sobre como redefinir sua senha em alguns minutos." 19 | sessions: 20 | bad_credentials: "Credenciais de login inválidas. Por favor, tente novamente." 21 | not_confirmed: "Um e-mail de confirmação foi enviado para sua conta em '%{email}'. Você deve seguir as instruções no e-mail antes que sua conta possa ser ativada" 22 | confirmations: 23 | already_confirmed: "O e-mail já foi confirmado, tente fazer login" 24 | invalid_token: "Token de confirmação inválido. Por favor, tente novamente" 25 | user_not_found: "Não foi possível encontrar o usuário com o e-mail '%{email}'." 26 | send_instructions: "Você receberá um e-mail com instruções sobre como confirmar seu endereço de e-mail em alguns minutos." 27 | mailer: 28 | confirmation_instructions: 29 | confirm_link_msg: "Você pode confirmar o e-mail da sua conta através do link abaixo:" 30 | confirm_account_link: "Confirmar minha conta" 31 | reset_password_instructions: 32 | request_reset_link_msg: "Alguém solicitou um link para alterar sua senha. Você pode fazer isso através do link abaixo." 33 | password_change_link: "Altere minha senha" 34 | ignore_mail_msg: "Se você não solicitou isso, ignore este e-mail." 35 | no_changes_msg: "Sua senha não será alterada até que você acesse o link acima e crie uma nova." 36 | unlock_instructions: 37 | account_lock_msg: "Sua conta foi bloqueada devido a um número excessivo de tentativas de login malsucedidas." 38 | hello: "olá" 39 | welcome: "bem-vindo(a)" 40 | 41 | -------------------------------------------------------------------------------- /spec/requests/mutations/send_password_reset_with_token_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'Send Password Reset Requests' do 6 | include_context 'with graphql query request' 7 | 8 | let!(:user) { create(:user, :confirmed, email: 'jwinnfield@wallaceinc.com') } 9 | let(:email) { user.email } 10 | let(:redirect_url) { 'https://google.com' } 11 | let(:query) do 12 | <<-GRAPHQL 13 | mutation { 14 | userSendPasswordResetWithToken( 15 | email: "#{email}", 16 | redirectUrl: "#{redirect_url}" 17 | ) { 18 | message 19 | } 20 | } 21 | GRAPHQL 22 | end 23 | 24 | context 'when redirect_url is not whitelisted' do 25 | let(:redirect_url) { 'https://not-safe.com' } 26 | 27 | it 'returns a not whitelisted redirect url error' do 28 | expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count) 29 | 30 | expect(json_response[:errors]).to containing_exactly( 31 | hash_including( 32 | message: "Redirect to '#{redirect_url}' not allowed.", 33 | extensions: { code: 'USER_ERROR' } 34 | ) 35 | ) 36 | end 37 | end 38 | 39 | context 'when params are correct' do 40 | context 'when using the gem schema' do 41 | it 'sends password reset email' do 42 | expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1) 43 | 44 | expect(json_response[:data][:userSendPasswordResetWithToken]).to include( 45 | message: 'You will receive an email with instructions on how to reset your password in a few minutes.' 46 | ) 47 | 48 | email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded) 49 | link = email.css('a').first 50 | 51 | expect(link['href']).to include(redirect_url + '?reset_password_token') 52 | end 53 | end 54 | end 55 | 56 | context 'when email address uses different casing' do 57 | let(:email) { 'jWinnfield@wallaceinc.com' } 58 | 59 | it 'honors devise configuration for case insensitive fields' do 60 | expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1) 61 | expect(json_response[:data][:userSendPasswordResetWithToken]).to include( 62 | message: 'You will receive an email with instructions on how to reset your password in a few minutes.' 63 | ) 64 | end 65 | end 66 | 67 | context 'when user email is not found' do 68 | let(:email) { 'nothere@gmail.com' } 69 | 70 | before { post_request } 71 | 72 | it 'returns an error' do 73 | expect(json_response[:errors]).to contain_exactly( 74 | hash_including(message: 'User was not found or was not logged in.', extensions: { code: 'USER_ERROR' }) 75 | ) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/devise_token_auth.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | DeviseTokenAuth.setup do |config| 4 | # By default the authorization headers will change after each request. The 5 | # client is responsible for keeping track of the changing tokens. Change 6 | # this to false to prevent the Authorization header from changing after 7 | # each request. 8 | config.change_headers_on_each_request = false 9 | 10 | # By default, users will need to re-authenticate after 2 weeks. This setting 11 | # determines how long tokens will remain valid after they are issued. 12 | # config.token_lifespan = 2.weeks 13 | 14 | # Limiting the token_cost to just 4 in testing will increase the performance of 15 | # your test suite dramatically. The possible cost value is within range from 4 16 | # to 31. It is recommended to not use a value more than 10 in other environments. 17 | # config.token_cost = Rails.env.test? ? 4 : 10 18 | 19 | # Sets the max number of concurrent devices per user, which is 10 by default. 20 | # After this limit is reached, the oldest tokens will be removed. 21 | # config.max_number_of_devices = 10 22 | 23 | # Sometimes it's necessary to make several requests to the API at the same 24 | # time. In this case, each request in the batch will need to share the same 25 | # auth token. This setting determines how far apart the requests can be while 26 | # still using the same auth token. 27 | # config.batch_request_buffer_throttle = 5.seconds 28 | 29 | # This route will be the prefix for all oauth2 redirect callbacks. For 30 | # example, using the default '/omniauth', the github oauth2 provider will 31 | # redirect successful authentications to '/omniauth/github/callback' 32 | # config.omniauth_prefix = "/omniauth" 33 | 34 | # By default sending current password is not needed for the password update. 35 | # Uncomment to enforce current_password param to be checked before all 36 | # attribute updates. Set it to :password if you want it to be checked only if 37 | # password is updated. 38 | config.check_current_password_before_update = :password 39 | 40 | config.default_confirm_success_url = 'https://google.com' 41 | 42 | config.redirect_whitelist = ['https://google.com'] 43 | 44 | # By default we will use callbacks for single omniauth. 45 | # It depends on fields like email, provider and uid. 46 | # config.default_callbacks = true 47 | 48 | # Makes it possible to change the headers names 49 | # config.headers_names = {:'access-token' => 'access-token', 50 | # :'client' => 'client', 51 | # :'expiry' => 'expiry', 52 | # :'uid' => 'uid', 53 | # :'token-type' => 'token-type' } 54 | 55 | # By default, only Bearer Token authentication is implemented out of the box. 56 | # If, however, you wish to integrate with legacy Devise authentication, you can 57 | # do so by enabling this flag. NOTE: This feature is highly experimental! 58 | # config.enable_standard_devise_support = false 59 | end 60 | -------------------------------------------------------------------------------- /spec/services/mount_method/option_sanitizers/hash_checker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OptionSanitizers::HashChecker do 6 | describe '#call!' do 7 | subject(:clean_value) { described_class.new(element_type).call!(value, key) } 8 | 9 | let(:key) { :any_option } 10 | 11 | context 'when a single valid type is provided' do 12 | let(:element_type) { Numeric } 13 | 14 | context 'when no value is provided' do 15 | let(:value) { nil } 16 | 17 | it { is_expected.to eq({}) } 18 | end 19 | 20 | context 'when provided value is not a hash' do 21 | let(:value) { 'not a hash' } 22 | 23 | it 'raises an error' do 24 | expect { clean_value }.to raise_error(GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has an invalid value. Hash expected. Got String.") 25 | end 26 | end 27 | 28 | context 'when provided hash contains invalid elements' do 29 | let(:value) { { valid: Float, invalid: String } } 30 | 31 | it 'raises an error' do 32 | expect { clean_value }.to raise_error(GraphqlDevise::InvalidMountOptionsError, "`#{key} -> invalid` option has an invalid value. #{element_type} or descendants expected. Got #{String}.") 33 | end 34 | end 35 | 36 | context 'when provided array contains all valid elements' do 37 | let(:value) { { valid1: Numeric, valid2: Numeric } } 38 | 39 | it { is_expected.to eq(value) } 40 | end 41 | 42 | context 'when provided class has the expected type as an acestor' do 43 | let(:value) { { valid: Float } } 44 | 45 | it { is_expected.to eq(value) } 46 | end 47 | end 48 | 49 | context 'when multiple value types are allowed' do 50 | let(:element_type) { [String, Numeric] } 51 | 52 | context 'when no value is provided' do 53 | let(:value) { nil } 54 | 55 | it { is_expected.to eq({}) } 56 | end 57 | 58 | context 'when provided array contains all valid elements' do 59 | let(:value) { { valid1: String, valid2: Numeric } } 60 | 61 | it { is_expected.to eq(value) } 62 | end 63 | 64 | context 'when provided class has the expected type as an acestor' do 65 | let(:value) { { valid: Float } } 66 | 67 | it { is_expected.to eq(value) } 68 | end 69 | 70 | context 'when provided value is not a hash' do 71 | let(:value) { 'not a hash' } 72 | 73 | it 'raises an error' do 74 | expect { clean_value }.to raise_error(GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has an invalid value. Hash expected. Got String.") 75 | end 76 | end 77 | 78 | context 'when provided hash contains invalid elements' do 79 | let(:value) { { valid: String, invalid: StandardError } } 80 | 81 | it 'raises an error' do 82 | expect { clean_value }.to raise_error(GraphqlDevise::InvalidMountOptionsError, "`#{key} -> invalid` option has an invalid value. #{element_type.join(', ')} or descendants expected. Got #{StandardError}.") 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/services/mount_method/option_sanitizer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OptionSanitizer do 6 | subject(:clean_options) { described_class.new(options, supported_options).call! } 7 | 8 | describe '#call!' do 9 | let(:supported_options) do 10 | { 11 | my_string: GraphqlDevise::MountMethod::OptionSanitizers::StringChecker.new('default string'), 12 | hash_multiple: GraphqlDevise::MountMethod::OptionSanitizers::HashChecker.new([String, Numeric]), 13 | array: GraphqlDevise::MountMethod::OptionSanitizers::ArrayChecker.new(Symbol), 14 | hash_single: GraphqlDevise::MountMethod::OptionSanitizers::HashChecker.new(Float), 15 | my_class: GraphqlDevise::MountMethod::OptionSanitizers::ClassChecker.new(Numeric) 16 | } 17 | end 18 | 19 | context 'when all options are provided and correct' do 20 | let(:options) do 21 | { 22 | my_string: 'non default', 23 | hash_multiple: { first: String, second: Float, third: Float }, 24 | array: [:one, :two, :three], 25 | hash_single: { first: Float, second: Float }, 26 | my_class: Float 27 | } 28 | end 29 | 30 | it 'returns a struct with clean options' do 31 | expect( 32 | my_string: clean_options.my_string, 33 | hash_multiple: clean_options.hash_multiple, 34 | array: clean_options.array, 35 | hash_single: clean_options.hash_single, 36 | my_class: clean_options.my_class 37 | ).to match( 38 | my_string: 'non default', 39 | hash_multiple: { first: String, second: Float, third: Float }, 40 | array: [:one, :two, :three], 41 | hash_single: { first: Float, second: Float }, 42 | my_class: Float 43 | ) 44 | end 45 | end 46 | 47 | context 'when some options are provided but all correct' do 48 | let(:options) do 49 | { 50 | hash_multiple: { first: String, second: Float, third: Float }, 51 | array: [:one, :two, :three], 52 | my_class: Float 53 | } 54 | end 55 | 56 | it 'returns a struct with clean options and default values' do 57 | expect( 58 | my_string: clean_options.my_string, 59 | hash_multiple: clean_options.hash_multiple, 60 | array: clean_options.array, 61 | hash_single: clean_options.hash_single, 62 | my_class: clean_options.my_class 63 | ).to match( 64 | my_string: 'default string', 65 | hash_multiple: { first: String, second: Float, third: Float }, 66 | array: [:one, :two, :three], 67 | hash_single: {}, 68 | my_class: Float 69 | ) 70 | end 71 | end 72 | 73 | context 'when an option provided is invalid' do 74 | let(:options) do 75 | { 76 | hash_multiple: { first: String, second: Float, third: Float }, 77 | array: ['not symbol 1', 'not symbol 2'], 78 | my_class: Float 79 | } 80 | end 81 | 82 | it 'raises an error' do 83 | expect { clean_options }.to raise_error(GraphqlDevise::InvalidMountOptionsError, '`array` option has invalid elements. Symbol expected.') 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 28 | # config.action_controller.asset_host = 'http://assets.example.com' 29 | 30 | # Specifies the header that your server uses for sending files. 31 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 32 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 33 | 34 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 35 | # config.force_ssl = true 36 | 37 | # Use the lowest log level to ensure availability of diagnostic information 38 | # when problems arise. 39 | config.log_level = :debug 40 | 41 | # Prepend all log lines with the following tags. 42 | config.log_tags = [ :request_id ] 43 | 44 | # Use a different cache store in production. 45 | # config.cache_store = :mem_cache_store 46 | 47 | # Use a real queuing backend for Active Job (and separate queues per environment) 48 | # config.active_job.queue_adapter = :resque 49 | # config.active_job.queue_name_prefix = "dummy_#{Rails.env}" 50 | 51 | # Ignore bad email addresses and do not raise email delivery errors. 52 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 53 | # config.action_mailer.raise_delivery_errors = false 54 | 55 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 56 | # the I18n.default_locale when a translation cannot be found). 57 | config.i18n.fallbacks = true 58 | 59 | # Send deprecation notices to registered listeners. 60 | config.active_support.deprecation = :notify 61 | 62 | # Use default logging formatter so that PID and timestamp are not suppressed. 63 | config.log_formatter = ::Logger::Formatter.new 64 | 65 | # Use a different logger for distributed setups. 66 | # require 'syslog/logger' 67 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 68 | 69 | if ENV["RAILS_LOG_TO_STDOUT"].present? 70 | logger = ActiveSupport::Logger.new(STDOUT) 71 | logger.formatter = config.log_formatter 72 | config.logger = ActiveSupport::TaggedLogging.new(logger) 73 | end 74 | 75 | # Do not dump schema after migrations. 76 | config.active_record.dump_schema_after_migration = false 77 | end 78 | -------------------------------------------------------------------------------- /spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::DefaultOperationPreparer do 6 | describe '#call' do 7 | subject(:prepared) { default_preparer.call } 8 | 9 | let(:default_preparer) { described_class.new(selected_operations: operations, custom_keys: custom_keys, model: model, preparer: preparer) } 10 | let(:confirm_operation) { double(:confirm_operation, graphql_name: nil) } 11 | let(:sign_up_operation) { double(:sign_up_operation, graphql_name: nil) } 12 | let(:login_operation) { double(:confirm_operation, graphql_name: nil) } 13 | let(:logout_operation) { double(:sign_up_operation, graphql_name: nil) } 14 | let(:model) { User } 15 | let(:preparer) { double(:preparer) } 16 | let(:custom_keys) { [:login, :logout] } 17 | let(:operations) do 18 | { 19 | confirm: { klass: confirm_operation, authenticatable: false }, 20 | sign_up: { klass: sign_up_operation, authenticatable: true }, 21 | login: { klass: login_operation, authenticatable: true }, 22 | logout: { klass: logout_operation, authenticatable: true } 23 | } 24 | end 25 | 26 | before do 27 | allow(default_preparer).to receive(:child_class).with(confirm_operation).and_return(confirm_operation) 28 | allow(default_preparer).to receive(:child_class).with(sign_up_operation).and_return(sign_up_operation) 29 | allow(default_preparer).to receive(:child_class).with(login_operation).and_return(login_operation) 30 | allow(default_preparer).to receive(:child_class).with(logout_operation).and_return(logout_operation) 31 | allow(preparer).to receive(:call).with(confirm_operation, authenticatable: false).and_return(confirm_operation) 32 | allow(preparer).to receive(:call).with(sign_up_operation, authenticatable: true).and_return(sign_up_operation) 33 | allow(preparer).to receive(:call).with(login_operation, authenticatable: true).and_return(login_operation) 34 | allow(preparer).to receive(:call).with(logout_operation, authenticatable: true).and_return(logout_operation) 35 | end 36 | 37 | it 'returns only those operations with no custom operation provided' do 38 | expect(prepared.keys).to contain_exactly(:user_sign_up, :user_confirm) 39 | end 40 | 41 | it 'prepares default operations' do 42 | expect(confirm_operation).to receive(:graphql_name).with('UserConfirm') 43 | expect(sign_up_operation).to receive(:graphql_name).with('UserSignUp') 44 | expect(preparer).to receive(:call).with(confirm_operation, authenticatable: false) 45 | expect(preparer).to receive(:call).with(sign_up_operation, authenticatable: true) 46 | 47 | prepared 48 | 49 | expect(confirm_operation.instance_variable_get(:@resource_klass)).to eq(User) 50 | expect(sign_up_operation.instance_variable_get(:@resource_klass)).to eq(User) 51 | end 52 | 53 | context 'when no custom keys are provided' do 54 | let(:custom_keys) { [] } 55 | 56 | it 'returns all selected operations' do 57 | expect(prepared.keys).to contain_exactly(:user_sign_up, :user_confirm, :user_login, :user_logout) 58 | end 59 | end 60 | 61 | context 'when no selected operations are provided' do 62 | let(:operations) { {} } 63 | 64 | it 'returns all selected operations' do 65 | expect(prepared.keys).to eq([]) 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/graphql/user_queries_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'Users controller specs' do 6 | include_context 'with graphql schema test' 7 | 8 | let(:schema) { DummySchema } 9 | let(:user) { create(:user, :confirmed) } 10 | let(:field) { 'privateField' } 11 | let(:public_message) { 'Field does not require authentication' } 12 | let(:private_message) { 'Field will always require authentication' } 13 | let(:private_error) do 14 | { 15 | message: "#{field} field requires authentication", 16 | extensions: { code: 'AUTHENTICATION_ERROR' } 17 | } 18 | end 19 | 20 | describe 'publicField' do 21 | let(:query) do 22 | <<-GRAPHQL 23 | query { 24 | publicField 25 | } 26 | GRAPHQL 27 | end 28 | 29 | context 'when using a regular schema' do 30 | it 'does not require authentication' do 31 | expect(response[:data][:publicField]).to eq(public_message) 32 | end 33 | end 34 | end 35 | 36 | describe 'privateField' do 37 | let(:query) do 38 | <<-GRAPHQL 39 | query { 40 | privateField 41 | } 42 | GRAPHQL 43 | end 44 | 45 | context 'when using a regular schema' do 46 | context 'when user is authenticated' do 47 | let(:resource) { user } 48 | 49 | it 'allows to perform the query' do 50 | expect(response[:data][:privateField]).to eq(private_message) 51 | end 52 | 53 | context 'when using a SchemaUser' do 54 | let(:resource) { create(:schema_user, :confirmed) } 55 | 56 | it 'allows to perform the query' do 57 | expect(response[:data][:privateField]).to eq(private_message) 58 | end 59 | end 60 | end 61 | end 62 | 63 | context 'when using an interpreter schema' do 64 | let(:schema) { InterpreterSchema } 65 | 66 | context 'when user is authenticated' do 67 | let(:resource) { user } 68 | 69 | it 'allows to perform the query' do 70 | expect(response[:data][:privateField]).to eq(private_message) 71 | end 72 | end 73 | end 74 | end 75 | 76 | describe 'user' do 77 | let(:user_data) { { email: user.email, id: user.id } } 78 | let(:query) do 79 | <<-GRAPHQL 80 | query { 81 | user( 82 | id: #{user.id} 83 | ) { 84 | id 85 | email 86 | } 87 | } 88 | GRAPHQL 89 | end 90 | 91 | context 'when using a regular schema' do 92 | context 'when user is authenticated' do 93 | let(:resource) { user } 94 | 95 | it 'allows to perform the query' do 96 | expect(response[:data][:user]).to match(**user_data) 97 | end 98 | end 99 | end 100 | 101 | context 'when using an interpreter schema' do 102 | let(:schema) { InterpreterSchema } 103 | 104 | context 'when user is authenticated' do 105 | let(:resource) { user } 106 | 107 | it 'allows to perform the query' do 108 | expect(response[:data][:user]).to match(**user_data) 109 | end 110 | end 111 | 112 | context 'when user is not authenticated' do 113 | # Interpreter schema fields are public unless specified otherwise (plugin setting) 114 | it 'allows to perform the query' do 115 | expect(response[:data][:user]).to match(**user_data) 116 | end 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/generators/graphql_devise/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | class InstallGenerator < ::Rails::Generators::Base 5 | source_root File.expand_path('templates', __dir__) 6 | 7 | argument :user_class, type: :string, default: 'User' 8 | argument :mount_path, type: :string, default: 'graphql_auth' 9 | 10 | class_option :mount, type: :string, default: 'separate_route' 11 | 12 | def execute_devise_installer 13 | generate 'devise:install' 14 | end 15 | 16 | def execute_dta_installer 17 | # Necessary in case of a re-run of the generator, for DTA to detect concerns already included 18 | if File.exist?(File.expand_path("app/models/#{user_class.underscore}.rb", destination_root)) 19 | gsub_file( 20 | "app/models/#{user_class.underscore}.rb", 21 | 'GraphqlDevise::Authenticatable', 22 | 'DeviseTokenAuth::Concerns::User' 23 | ) 24 | end 25 | gsub_file( 26 | 'app/controllers/application_controller.rb', 27 | 'GraphqlDevise::SetUserByToken', 28 | 'DeviseTokenAuth::Concerns::SetUserByToken' 29 | ) 30 | 31 | generate 'devise_token_auth:install', "#{user_class} #{mount_path}" 32 | end 33 | 34 | def mount_resource_route 35 | routes_file = 'config/routes.rb' 36 | dta_route = "mount_devise_token_auth_for '#{user_class}', at: '#{mount_path}'" 37 | 38 | if options['mount'] != 'separate_route' 39 | gsub_file(routes_file, /^\s+#{Regexp.escape(dta_route + "\n")}/i, '') 40 | else 41 | gem_route = "mount_graphql_devise_for #{user_class}, at: '#{mount_path}'" 42 | 43 | if file_contains_str?(routes_file, gem_route) 44 | gsub_file(routes_file, /^\s+#{Regexp.escape(dta_route + "\n")}/i, '') 45 | 46 | say_status('skipped', "Routes already exist for #{user_class} at #{mount_path}") 47 | else 48 | gsub_file(routes_file, /#{Regexp.escape(dta_route)}/i, gem_route) 49 | end 50 | end 51 | end 52 | 53 | def replace_model_concern 54 | gsub_file( 55 | "app/models/#{user_class.underscore}.rb", 56 | /^\s+include DeviseTokenAuth::Concerns::User/, 57 | ' include GraphqlDevise::Authenticatable' 58 | ) 59 | end 60 | 61 | def replace_controller_concern 62 | gsub_file( 63 | 'app/controllers/application_controller.rb', 64 | /^\s+include DeviseTokenAuth::Concerns::SetUserByToken/, 65 | ' include GraphqlDevise::SetUserByToken' 66 | ) 67 | end 68 | 69 | def set_change_headers_on_each_request_false 70 | gsub_file( 71 | 'config/initializers/devise_token_auth.rb', 72 | '# config.change_headers_on_each_request = true', 73 | 'config.change_headers_on_each_request = false' 74 | ) 75 | end 76 | 77 | def mount_in_schema 78 | return if options['mount'] == 'separate_route' 79 | 80 | inject_into_file "app/graphql/#{options['mount'].underscore}.rb", after: "< GraphQL::Schema\n" do 81 | <<-RUBY 82 | use GraphqlDevise::SchemaPlugin.new( 83 | query: Types::QueryType, 84 | mutation: Types::MutationType, 85 | resource_loaders: [ 86 | GraphqlDevise::ResourceLoader.new(#{user_class}) 87 | ] 88 | ) 89 | RUBY 90 | end 91 | end 92 | 93 | private 94 | 95 | def file_contains_str?(filename, regex_str) 96 | path = File.join(destination_root, filename) 97 | 98 | File.read(path) =~ /(#{Regexp.escape(regex_str)})/i 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/graphql_devise/concerns/controller_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | module ControllerMethods 5 | extend ActiveSupport::Concern 6 | 7 | private 8 | 9 | def check_redirect_url_whitelist!(redirect_url) 10 | if blacklisted_redirect_url?(redirect_url) 11 | raise_user_error(I18n.t('graphql_devise.redirect_url_not_allowed', redirect_url: redirect_url)) 12 | end 13 | end 14 | 15 | def raise_user_error(message) 16 | raise UserError, message 17 | end 18 | 19 | def raise_user_error_list(message, resource:) 20 | raise DetailedUserError.new(message, errors: resource.errors.full_messages) 21 | end 22 | 23 | def remove_resource 24 | controller.resource = nil 25 | controller.client_id = nil 26 | controller.token = nil 27 | end 28 | 29 | def response 30 | controller.response 31 | end 32 | 33 | def controller 34 | context[:controller] 35 | end 36 | 37 | def resource_name 38 | ::GraphqlDevise.to_mapping_name(resource_class) 39 | end 40 | 41 | def resource_class 42 | self.class.instance_variable_get(:@resource_klass) 43 | end 44 | 45 | def recoverable_enabled? 46 | resource_class.devise_modules.include?(:recoverable) 47 | end 48 | 49 | def confirmable_enabled? 50 | resource_class.devise_modules.include?(:confirmable) 51 | end 52 | 53 | def blacklisted_redirect_url?(redirect_url) 54 | DeviseTokenAuth.redirect_whitelist && !DeviseTokenAuth::Url.whitelisted?(redirect_url) 55 | end 56 | 57 | def current_resource 58 | @current_resource ||= controller.send(:set_resource_by_token, resource_class) 59 | end 60 | 61 | def client 62 | if Gem::Version.new(DeviseTokenAuth::VERSION) <= Gem::Version.new('1.1.0') 63 | controller.client_id 64 | else 65 | controller.token.client if controller.token.present? 66 | end 67 | end 68 | 69 | def generate_auth_headers(resource) 70 | auth_headers = resource.create_new_auth_token 71 | controller.resource = resource 72 | access_token_name = DeviseTokenAuth.headers_names[:'access-token'] 73 | client_name = DeviseTokenAuth.headers_names[:'client'] 74 | 75 | # NOTE: Depending on the DTA version, the token will be an object or nil 76 | if controller.token 77 | controller.token.client = auth_headers[client_name] 78 | controller.token.token = auth_headers[access_token_name] 79 | else 80 | controller.client_id = auth_headers[client_name] 81 | controller.token = auth_headers[access_token_name] 82 | end 83 | 84 | auth_headers 85 | end 86 | 87 | def find_resource(field, value) 88 | if resource_class.respond_to?(:connection) && resource_class.connection.adapter_name.downcase.include?('mysql') 89 | # fix for mysql default case insensitivity 90 | resource_class.where("BINARY #{field} = ? AND provider= ?", value, provider).first 91 | elsif Gem::Version.new(DeviseTokenAuth::VERSION) < Gem::Version.new('1.1.0') 92 | resource_class.find_by(field => value, :provider => provider) 93 | else 94 | resource_class.dta_find_by(field => value, :provider => provider) 95 | end 96 | end 97 | 98 | def get_case_insensitive_field(field, value) 99 | if resource_class.case_insensitive_keys.include?(field) 100 | value.downcase 101 | else 102 | value 103 | end 104 | end 105 | 106 | def provider 107 | :email 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/requests/mutations/confirm_registration_with_token_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'Registration confirmation with token' do 6 | include_context 'with graphql query request' 7 | 8 | context 'when using the user model' do 9 | let(:user) { create(:user, confirmed_at: nil) } 10 | let(:query) do 11 | <<-GRAPHQL 12 | mutation { 13 | userConfirmRegistrationWithToken( 14 | confirmationToken: "#{token}" 15 | ) { 16 | authenticatable { 17 | email 18 | name 19 | } 20 | credentials { client } 21 | } 22 | } 23 | GRAPHQL 24 | end 25 | 26 | context 'when confirmation token is correct' do 27 | let(:token) { user.confirmation_token } 28 | 29 | before do 30 | user.send_confirmation_instructions( 31 | template_path: ['graphql_devise/mailer'] 32 | ) 33 | end 34 | 35 | it 'confirms the resource and returns credentials' do 36 | expect do 37 | post_request 38 | user.reload 39 | end.to(change(user, :confirmed_at).from(nil)) 40 | 41 | expect(json_response[:data][:userConfirmRegistrationWithToken]).to include( 42 | authenticatable: { email: user.email, name: user.name }, 43 | credentials: { client: user.tokens.keys.first } 44 | ) 45 | 46 | expect(user).to be_active_for_authentication 47 | end 48 | 49 | context 'when unconfirmed_email is present' do 50 | let(:user) { create(:user, :confirmed, unconfirmed_email: 'vvega@wallaceinc.com') } 51 | 52 | it 'confirms the unconfirmed email' do 53 | expect do 54 | post_request 55 | user.reload 56 | end.to change(user, :email).from(user.email).to('vvega@wallaceinc.com').and( 57 | change(user, :unconfirmed_email).from('vvega@wallaceinc.com').to(nil) 58 | ) 59 | end 60 | end 61 | end 62 | 63 | context 'when reset password token is not found' do 64 | let(:token) { "#{user.confirmation_token}-invalid" } 65 | 66 | it 'does *NOT* confirm the user' do 67 | expect do 68 | post_request 69 | user.reload 70 | end.not_to change(user, :confirmed_at).from(nil) 71 | 72 | expect(json_response[:errors]).to contain_exactly( 73 | hash_including( 74 | message: 'Invalid confirmation token. Please try again', 75 | extensions: { code: 'USER_ERROR' } 76 | ) 77 | ) 78 | end 79 | end 80 | end 81 | 82 | context 'when using the admin model' do 83 | let(:admin) { create(:admin, confirmed_at: nil) } 84 | let(:query) do 85 | <<-GRAPHQL 86 | mutation { 87 | adminConfirmRegistrationWithToken( 88 | confirmationToken: "#{token}" 89 | ) { 90 | authenticatable { email } 91 | } 92 | } 93 | GRAPHQL 94 | end 95 | 96 | context 'when confirmation token is correct' do 97 | let(:token) { admin.confirmation_token } 98 | 99 | before do 100 | admin.send_confirmation_instructions( 101 | template_path: ['graphql_devise/mailer'] 102 | ) 103 | end 104 | 105 | it 'confirms the resource and persists credentials on the DB' do 106 | expect do 107 | get_request 108 | admin.reload 109 | end.to change(admin, :confirmed_at).from(nil).and( 110 | change { admin.tokens.keys.count }.from(0).to(1) 111 | ) 112 | 113 | expect(admin).to be_active_for_authentication 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /spec/requests/mutations/update_password_with_token_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'Update Password With Token' do 6 | include_context 'with graphql query request' 7 | 8 | let(:password) { '12345678' } 9 | let(:password_confirmation) { password } 10 | 11 | context 'when using the user model' do 12 | let(:user) { create(:user, :confirmed) } 13 | let(:query) do 14 | <<-GRAPHQL 15 | mutation { 16 | userUpdatePasswordWithToken( 17 | resetPasswordToken: "#{token}", 18 | password: "#{password}", 19 | passwordConfirmation: "#{password_confirmation}" 20 | ) { 21 | authenticatable { email } 22 | credentials { accessToken } 23 | } 24 | } 25 | GRAPHQL 26 | end 27 | 28 | context 'when reset password token is valid' do 29 | let(:token) { user.send(:set_reset_password_token) } 30 | 31 | it 'updates the password' do 32 | expect do 33 | post_request 34 | user.reload 35 | end.to change(user, :encrypted_password) 36 | 37 | expect(user).to be_valid_password(password) 38 | expect(json_response[:data][:userUpdatePasswordWithToken][:credentials]).to be_nil 39 | expect(json_response[:data][:userUpdatePasswordWithToken][:authenticatable]).to include(email: user.email) 40 | end 41 | 42 | context 'when token has expired' do 43 | it 'returns an expired token error' do 44 | travel_to 10.hours.ago do 45 | token 46 | end 47 | 48 | post_request 49 | 50 | expect(json_response[:errors]).to contain_exactly( 51 | hash_including(message: 'Reset password token is no longer valid.', extensions: { code: 'USER_ERROR' }) 52 | ) 53 | end 54 | end 55 | 56 | context 'when password confirmation does not match' do 57 | let(:password_confirmation) { 'does not match' } 58 | 59 | it 'returns an error' do 60 | post_request 61 | 62 | expect(json_response[:errors]).to contain_exactly( 63 | hash_including( 64 | message: 'Unable to update user password', 65 | extensions: { code: 'USER_ERROR', detailed_errors: ["Password confirmation doesn't match Password"] } 66 | ) 67 | ) 68 | end 69 | end 70 | end 71 | 72 | context 'when reset password token is not found' do 73 | let(:token) { user.send(:set_reset_password_token) + 'invalid' } 74 | 75 | it 'returns an error' do 76 | post_request 77 | 78 | expect(json_response[:errors]).to contain_exactly( 79 | hash_including(message: 'No user found for the specified reset token.', extensions: { code: 'USER_ERROR' }) 80 | ) 81 | end 82 | end 83 | end 84 | 85 | context 'when using the admin model' do 86 | let(:admin) { create(:admin, :confirmed) } 87 | let(:query) do 88 | <<-GRAPHQL 89 | mutation { 90 | adminUpdatePasswordWithToken( 91 | resetPasswordToken: "#{token}", 92 | password: "#{password}", 93 | passwordConfirmation: "#{password_confirmation}" 94 | ) { 95 | authenticatable { email } 96 | credentials { uid } 97 | } 98 | } 99 | GRAPHQL 100 | end 101 | 102 | context 'when reset password token is valid' do 103 | let(:token) { admin.send(:set_reset_password_token) } 104 | 105 | it 'updates the password' do 106 | expect do 107 | post_request 108 | admin.reload 109 | end.to change(admin, :encrypted_password) 110 | 111 | expect(admin).to be_valid_password(password) 112 | expect(json_response[:data][:adminUpdatePasswordWithToken]).to include( 113 | credentials: { uid: admin.email }, 114 | authenticatable: { email: admin.email } 115 | ) 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/generators/graphql_devise/install_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Generators are not automatically loaded by Rails 4 | require 'rails_helper' 5 | require 'generators/graphql_devise/install_generator' 6 | 7 | # NOTE: Skipping tests because of a problem between zeitwerk and GQL versions 2.1.0 & 2.2.14 8 | skip_generator_tests = 9 | Gem::Version.new(GraphQL::VERSION).between?( 10 | Gem::Version.new('2.1.0'), 11 | Gem::Version.new('2.2.14') 12 | ) && Gem::Version.new(Rails.version) >= Gem::Version.new('7.0.0') 13 | 14 | RSpec.describe GraphqlDevise::InstallGenerator, type: :generator, skip: skip_generator_tests do 15 | destination File.expand_path('../../../../gqld_dummy', __dir__) 16 | 17 | let(:routes_path) { "#{destination_root}/config/routes.rb" } 18 | let(:routes_content) { File.read(routes_path) } 19 | let(:dta_route) { 'mount_devise_token_auth_for' } 20 | 21 | after(:all) { FileUtils.rm_rf(destination_root) } 22 | 23 | before do 24 | prepare_destination 25 | create_rails_project 26 | run_generator(args) 27 | end 28 | 29 | context 'when mount option is schema' do 30 | let(:args) { ['Admin', '--mount', 'GqldDummySchema'] } 31 | 32 | it 'mounts the SchemaPlugin' do 33 | assert_file 'config/initializers/devise.rb' 34 | assert_file 'config/initializers/devise_token_auth.rb', /^\s{2}#{Regexp.escape('config.change_headers_on_each_request = false')}/ 35 | assert_file 'config/locales/devise.en.yml' 36 | 37 | assert_migration 'db/migrate/devise_token_auth_create_admins.rb' 38 | 39 | assert_file 'app/models/admin.rb', /^\s{2}devise :.+include GraphqlDevise::Authenticatable/m 40 | 41 | assert_file 'app/controllers/application_controller.rb', /^\s{2}include GraphqlDevise::SetUserByToken/ 42 | 43 | assert_file 'app/graphql/gqld_dummy_schema.rb', /\s+#{Regexp.escape("GraphqlDevise::ResourceLoader.new(Admin)")}/ 44 | end 45 | end 46 | 47 | context 'when passing no params to the generator' do 48 | let(:args) { [] } 49 | 50 | it 'creates and updated required files' do 51 | assert_file 'config/routes.rb', /^\s{2}mount_graphql_devise_for User, at: 'graphql_auth'/ 52 | expect(routes_content).not_to match(dta_route) 53 | 54 | assert_file 'config/initializers/devise.rb' 55 | assert_file 'config/initializers/devise_token_auth.rb', /^\s{2}#{Regexp.escape('config.change_headers_on_each_request = false')}/ 56 | assert_file 'config/locales/devise.en.yml' 57 | 58 | assert_migration 'db/migrate/devise_token_auth_create_users.rb' 59 | 60 | assert_file 'app/models/user.rb', /^\s{2}devise :.+include GraphqlDevise::Authenticatable/m 61 | 62 | assert_file 'app/controllers/application_controller.rb', /^\s{2}include GraphqlDevise::SetUserByToken/ 63 | end 64 | end 65 | 66 | context 'when passing custom params to the generator' do 67 | let(:args) { %w[Admin api] } 68 | 69 | it 'creates and updated required files' do 70 | assert_file 'config/routes.rb', /^\s{2}mount_graphql_devise_for Admin, at: 'api'/ 71 | expect(routes_content).not_to match(dta_route) 72 | 73 | assert_file 'config/initializers/devise.rb' 74 | assert_file 'config/initializers/devise_token_auth.rb', /^\s{2}#{Regexp.escape('config.change_headers_on_each_request = false')}/ 75 | assert_file 'config/locales/devise.en.yml' 76 | 77 | assert_migration 'db/migrate/devise_token_auth_create_admins.rb' 78 | 79 | assert_file 'app/models/admin.rb', /^\s{2}devise :.+include GraphqlDevise::Authenticatable/m 80 | 81 | assert_file 'app/controllers/application_controller.rb', /^\s{2}include GraphqlDevise::SetUserByToken/ 82 | end 83 | end 84 | 85 | def create_rails_project 86 | FileUtils.cd(File.join(destination_root, '..')) do 87 | `rails new gqld_dummy -S -C --skip-action-mailbox --skip-action-text -T --skip-spring --skip-bundle --skip-keeps -G --skip-active-storage -J --skip-listen --skip-bootsnap` 88 | end 89 | FileUtils.cd(File.join(destination_root, '../gqld_dummy')) do 90 | `rm -f config/initializers/assets.rb` 91 | end 92 | FileUtils.cd(File.join(destination_root, '../gqld_dummy')) do 93 | `rails generate graphql:install` 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /docs/usage/reset_password_flow.md: -------------------------------------------------------------------------------- 1 | # Reset Password Flow 2 | This gem supports two different ways to reset a password on a resource. Each password reset flow has it's own set of 3 | operations and this document will explain in more detail how to use each. 4 | The first and most recently implemented flow is preferred as it requires less steps and doesn't require a mutation 5 | to return a redirect on the response. Flow 2 might be deprecated in the future. 6 | 7 | ## Flow #1 (Preferred) 8 | This flow only has two steps. Each step name refers to the operation name you can use in the mount options to skip or override. 9 | 10 | ### 1. send_password_reset_with_token 11 | This mutation will send an email to the specified address if it's found on the system. Returns an error if the email is not found. Here's an example assuming the resource used 12 | for authentication is `User`: 13 | ```graphql 14 | mutation { 15 | userSendPasswordResetWithToken( 16 | email: "vvega@wallaceinc.com", 17 | redirectUrl: "https://google.com" 18 | ) { 19 | message 20 | } 21 | } 22 | ``` 23 | The email will contain a link to the `redirectUrl` (https://google.com in the example) and append a `reset_password_token` query param. This is the token you will 24 | need to use in the next step in order to reset the password. 25 | 26 | ### 2. update_password_with_token 27 | This mutation uses the token sent on the email to find the resource you are trying to recover. 28 | All you have to do is send a valid token together with the new password and password confirmation. 29 | Here's an example assuming the resource used for authentication is `User`: 30 | 31 | ```graphql 32 | mutation { 33 | userUpdatePasswordWithToken( 34 | resetPasswordToken: "token_here", 35 | password: "password123", 36 | passwordConfirmation: "password123" 37 | ) { 38 | authenticatable { email } 39 | credentials { accessToken } 40 | } 41 | } 42 | ``` 43 | The mutation has two fields: 44 | 1. `authenticatable`: Just like other mutations, returns the actual resource you just recover the password for. 45 | 1. `credentials`: This is a nullable field. It will only return credentials as if you had just logged 46 | in into the app if you explicitly say so by overriding the mutation. The docs have more detail 47 | on how to extend the default behavior of mutations, but 48 | [here](https://github.com/graphql-devise/graphql_devise/blob/8c7c8a5ff1b35fb026e4c9499c70dc5f90b9187a/spec/dummy/app/graphql/mutations/reset_admin_password_with_token.rb) 49 | you can find an example mutation on what needs to be done in order for the mutation to return 50 | credentials after updating the password. 51 | 52 | ## Flow 2 (Deprecated) 53 | This was the first flow to be implemented, requires an additional step and also to encode a GQL query in a url, so this is not the preferred method. 54 | Each step name refers to the operation name you can use in the mount options to skip or override. 55 | 56 | ### 1. send_password_reset 57 | This mutation will send an email to the specified address if it's found on the system. Returns an error if the email is not found. Here's an example assuming the resource used 58 | for authentication is `User`: 59 | ```graphql 60 | mutation { 61 | userSendPasswordReset( 62 | email: "vvega@wallaceinc.com", 63 | redirectUrl: "https://google.com" 64 | ) { 65 | message 66 | } 67 | } 68 | ``` 69 | The email will contain an encoded GraphQL query that holds the reset token and redirectUrl. 70 | The query is described in the next step. 71 | 72 | ### 2. check_password_token 73 | This query checks the reset password token and if successful changes a column in the DB (`allow_password_change`) to true. 74 | This change will allow for the next step to update the password without providing the current password. 75 | Then, this query will redirect to the provided `redirectUrl` with credentials. 76 | 77 | ### 3. update_password 78 | This step requires the request to include authentication headers and will allow the user to 79 | update the password if step 2 was successful. 80 | Here's an example assuming the resource used for authentication is `User`: 81 | ```graphql 82 | mutation { 83 | userUpdatePassword( 84 | password: "password123", 85 | passwordConfirmation: "password123" 86 | ) { 87 | authenticatable { email } 88 | } 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /lib/graphql_devise/schema_plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphqlDevise 4 | class SchemaPlugin 5 | # NOTE: Based on GQL-Ruby docs https://graphql-ruby.org/schema/introspection.html 6 | INTROSPECTION_FIELDS = ['__schema', '__type', '__typename'] 7 | DEFAULT_NOT_AUTHENTICATED = ->(field) { raise AuthenticationError, "#{field} field requires authentication" } 8 | 9 | def initialize(query: nil, mutation: nil, authenticate_default: true, public_introspection: !Rails.env.production?, resource_loaders: [], unauthenticated_proc: DEFAULT_NOT_AUTHENTICATED) 10 | @query = query 11 | @mutation = mutation 12 | @resource_loaders = resource_loaders 13 | @authenticate_default = authenticate_default 14 | @public_introspection = public_introspection 15 | @unauthenticated_proc = unauthenticated_proc 16 | 17 | # Must happen on initialize so operations are loaded before the types are added to the schema on GQL < 1.10 18 | load_fields 19 | end 20 | 21 | def use(schema_definition) 22 | if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('2.3') 23 | schema_definition.trace_with( 24 | FieldAuthTracer, 25 | authenticate_default: @authenticate_default, 26 | public_introspection: public_introspection, 27 | unauthenticated_proc: @unauthenticated_proc 28 | ) 29 | else 30 | schema_definition.tracer(self) 31 | end 32 | end 33 | 34 | def trace(event, trace_data) 35 | # Authenticate only root level queries 36 | return yield unless event == 'execute_field' && path(trace_data).count == 1 37 | 38 | field = traced_field(trace_data) 39 | auth_required = authenticate_option(field, trace_data) 40 | context = context_from_data(trace_data) 41 | 42 | if auth_required && !(public_introspection && introspection_field?(field)) 43 | raise_on_missing_resource(context, field, auth_required) 44 | end 45 | 46 | yield 47 | end 48 | 49 | private 50 | 51 | attr_reader :public_introspection 52 | 53 | def raise_on_missing_resource(context, field, auth_required) 54 | @unauthenticated_proc.call(field.name) if context[:current_resource].blank? 55 | 56 | if auth_required.respond_to?(:call) && !auth_required.call(context[:current_resource]) 57 | @unauthenticated_proc.call(field.name) 58 | end 59 | end 60 | 61 | def context_from_data(trace_data) 62 | query = if trace_data[:context] 63 | trace_data[:context].query 64 | else 65 | trace_data[:query] 66 | end 67 | 68 | query.context 69 | end 70 | 71 | def path(trace_data) 72 | if trace_data[:context] 73 | trace_data[:context].path 74 | else 75 | trace_data[:path] 76 | end 77 | end 78 | 79 | def traced_field(trace_data) 80 | if trace_data[:context] 81 | trace_data[:context].field 82 | else 83 | trace_data[:field] 84 | end 85 | end 86 | 87 | def authenticate_option(field, trace_data) 88 | auth_required = if trace_data[:context] 89 | field.metadata[:authenticate] 90 | else 91 | if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('2.0') 92 | # authenticate will only be defined if "field_class GraphqlDevise::Types::BaseField" is added to the type 93 | # returning nil here will use the default value used when mounting the plugin 94 | field.try(:authenticate) 95 | elsif Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.13.1') 96 | field.graphql_definition(silence_deprecation_warning: true).metadata[:authenticate] 97 | else 98 | field.graphql_definition.metadata[:authenticate] 99 | end 100 | end 101 | 102 | auth_required.nil? ? @authenticate_default : auth_required 103 | end 104 | 105 | def load_fields 106 | @resource_loaders.each do |resource_loader| 107 | raise ::GraphqlDevise::Error, 'Invalid resource loader instance' unless resource_loader.instance_of?(ResourceLoader) 108 | 109 | resource_loader.call(@query, @mutation) 110 | end 111 | end 112 | 113 | def introspection_field?(field) 114 | INTROSPECTION_FIELDS.include?(field.name) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | coveralls: coveralls/coveralls@1.0.6 4 | 5 | jobs: 6 | test: 7 | parameters: 8 | ruby-version: 9 | type: string 10 | gemfile: 11 | type: string 12 | docker: 13 | - image: 'ruby:<< parameters.ruby-version >>' 14 | environment: 15 | BUNDLE_GEMFILE: << parameters.gemfile >> 16 | COVERALLS_PARALLEL: 'true' 17 | EAGER_LOAD: 'true' 18 | RUBYOPT: '-rostruct' 19 | steps: 20 | - checkout 21 | - run: ruby bin/install_bundler.rb 22 | - run: 23 | name: Install dependencies 24 | command: bundle install 25 | - run: 26 | name: Run Specs 27 | command: 28 | bundle exec rspec 29 | report-coverage: 30 | docker: 31 | - image: 'cimg/node:22.1.0' 32 | steps: 33 | - coveralls/upload: 34 | parallel_finished: true 35 | 36 | workflows: 37 | test-suite: 38 | jobs: 39 | - test: 40 | matrix: 41 | parameters: 42 | ruby-version: 43 | - '3.0' 44 | - '3.1' 45 | - '3.2' 46 | - '3.3' 47 | - '3.4' 48 | gemfile: 49 | - gemfiles/rails6.1_graphql1.11.gemfile 50 | - gemfiles/rails6.1_graphql1.12.gemfile 51 | - gemfiles/rails6.1_graphql1.13.gemfile 52 | - gemfiles/rails6.1_graphql2.0.gemfile 53 | - gemfiles/rails7.0_graphql2.0.gemfile 54 | - gemfiles/rails7.0_graphql2.1.gemfile 55 | - gemfiles/rails7.0_graphql2.2.gemfile 56 | - gemfiles/rails7.0_graphql2.3.gemfile 57 | - gemfiles/rails7.0_graphql2.4.gemfile 58 | - gemfiles/rails7.1_graphql2.0.gemfile 59 | - gemfiles/rails7.1_graphql2.1.gemfile 60 | - gemfiles/rails7.1_graphql2.2.gemfile 61 | - gemfiles/rails7.1_graphql2.3.gemfile 62 | - gemfiles/rails7.1_graphql2.4.gemfile 63 | - gemfiles/rails7.2_graphql2.0.gemfile 64 | - gemfiles/rails7.2_graphql2.1.gemfile 65 | - gemfiles/rails7.2_graphql2.2.gemfile 66 | - gemfiles/rails7.2_graphql2.3.gemfile 67 | - gemfiles/rails7.2_graphql2.4.gemfile 68 | - gemfiles/rails7.2_graphql2.5.gemfile 69 | - gemfiles/rails8.0_graphql2.2.gemfile 70 | - gemfiles/rails8.0_graphql2.3.gemfile 71 | - gemfiles/rails8.0_graphql2.4.gemfile 72 | - gemfiles/rails8.0_graphql2.5.gemfile 73 | exclude: 74 | - ruby-version: '3.0' 75 | gemfile: gemfiles/rails7.2_graphql2.0.gemfile 76 | - ruby-version: '3.0' 77 | gemfile: gemfiles/rails7.2_graphql2.1.gemfile 78 | - ruby-version: '3.0' 79 | gemfile: gemfiles/rails7.2_graphql2.2.gemfile 80 | - ruby-version: '3.0' 81 | gemfile: gemfiles/rails7.2_graphql2.3.gemfile 82 | - ruby-version: '3.0' 83 | gemfile: gemfiles/rails7.2_graphql2.4.gemfile 84 | - ruby-version: '3.0' 85 | gemfile: gemfiles/rails7.2_graphql2.5.gemfile 86 | - ruby-version: '3.0' 87 | gemfile: gemfiles/rails8.0_graphql2.2.gemfile 88 | - ruby-version: '3.0' 89 | gemfile: gemfiles/rails8.0_graphql2.3.gemfile 90 | - ruby-version: '3.0' 91 | gemfile: gemfiles/rails8.0_graphql2.4.gemfile 92 | - ruby-version: '3.0' 93 | gemfile: gemfiles/rails8.0_graphql2.5.gemfile 94 | - ruby-version: '3.1' 95 | gemfile: gemfiles/rails8.0_graphql2.2.gemfile 96 | - ruby-version: '3.1' 97 | gemfile: gemfiles/rails8.0_graphql2.3.gemfile 98 | - ruby-version: '3.1' 99 | gemfile: gemfiles/rails8.0_graphql2.4.gemfile 100 | - ruby-version: '3.1' 101 | gemfile: gemfiles/rails8.0_graphql2.5.gemfile 102 | - ruby-version: '3.2' 103 | gemfile: gemfiles/rails6.1_graphql1.11.gemfile 104 | - ruby-version: '3.3' 105 | gemfile: gemfiles/rails6.1_graphql1.11.gemfile 106 | - ruby-version: '3.4' 107 | gemfile: gemfiles/rails6.1_graphql1.11.gemfile 108 | - report-coverage: 109 | requires: 110 | - test 111 | --------------------------------------------------------------------------------