From 941cf6853466809a19c518463061113b29c01b8f Mon Sep 17 00:00:00 2001 From: Mario Celi Date: Sat, 1 May 2021 21:02:05 -0500 Subject: [PATCH 1/4] Allow controller level authentication --- .../concerns/set_user_by_token.rb | 38 ++++++++++++++++++- app/helpers/graphql_devise/mailer_helper.rb | 4 +- lib/graphql_devise.rb | 11 +++++- lib/graphql_devise/resource_loader.rb | 2 +- lib/graphql_devise/schema_plugin.rb | 7 +--- .../controllers/api/v1/graphql_controller.rb | 8 ++++ spec/dummy/config/routes.rb | 1 + spec/graphql/user_queries_spec.rb | 4 +- spec/requests/user_controller_spec.rb | 10 +++++ 9 files changed, 73 insertions(+), 12 deletions(-) diff --git a/app/controllers/graphql_devise/concerns/set_user_by_token.rb b/app/controllers/graphql_devise/concerns/set_user_by_token.rb index 5e93407c..9c2bb0c3 100644 --- a/app/controllers/graphql_devise/concerns/set_user_by_token.rb +++ b/app/controllers/graphql_devise/concerns/set_user_by_token.rb @@ -7,6 +7,39 @@ module Concerns SetUserByToken.module_eval do attr_accessor :client_id, :token, :resource + class_methods do + def set_resource_by_model(models, **kwargs) + Array(models).each do |model| + GraphqlDevise.add_mapping(GraphqlDevise.to_mapping_name(model.to_s), model.to_s) + end + GraphqlDevise.reconfigure_warden! + + before_action(**kwargs) do + authenticate_model(models) + end + end + end + + def authenticate_model(models) + Array(models).each do |model| + set_resource_by_token(model) + return @resource + end + + nil + end + + def resource_class(resource = nil) + return super unless resource.is_a?(Class) + + if (Object.const_defined?('ActiveRecord::Base') && resource < ActiveRecord::Base) || + (Object.const_defined?('Mongoid::Document') && resource < Mongoid::Document) + return resource + end + + super + end + def full_url_without_params request.base_url + request.path end @@ -16,10 +49,13 @@ def set_resource_by_token(resource) end def graphql_context(resource_name) - { + context = { resource_name: resource_name, controller: self } + context[:current_resource] = @resource if @resource.present? + + context end def build_redirect_headers(access_token, client, redirect_header_options = {}) diff --git a/app/helpers/graphql_devise/mailer_helper.rb b/app/helpers/graphql_devise/mailer_helper.rb index cc40b50e..d8c7faee 100644 --- a/app/helpers/graphql_devise/mailer_helper.rb +++ b/app/helpers/graphql_devise/mailer_helper.rb @@ -3,7 +3,7 @@ module GraphqlDevise module MailerHelper def confirmation_query(resource_name:, token:, redirect_url:) - name = "#{resource_name.underscore.tr('/', '_').camelize(:lower)}ConfirmAccount" + name = "#{GraphqlDevise.to_mapping_name(resource_name).camelize(:lower)}ConfirmAccount" raw = <<-GRAPHQL query($token:String!,$redirectUrl:String!){ #{name}(confirmationToken:$token,redirectUrl:$redirectUrl){ @@ -19,7 +19,7 @@ def confirmation_query(resource_name:, token:, redirect_url:) end def password_reset_query(token:, redirect_url:, resource_name:) - name = "#{resource_name.underscore.tr('/', '_').camelize(:lower)}CheckPasswordToken" + name = "#{GraphqlDevise.to_mapping_name(resource_name).camelize(:lower)}CheckPasswordToken" raw = <<-GRAPHQL query($token:String!,$redirectUrl:String!){ #{name}(resetPasswordToken:$token,redirectUrl:$redirectUrl){ diff --git a/lib/graphql_devise.rb b/lib/graphql_devise.rb index ca875e43..53eb0739 100644 --- a/lib/graphql_devise.rb +++ b/lib/graphql_devise.rb @@ -29,13 +29,22 @@ def self.mount_resource(mapping_name) end def self.add_mapping(mapping_name, resource) - return if Devise.mappings.key?(mapping_name) + return if Devise.mappings.key?(mapping_name.to_sym) Devise.add_mapping( mapping_name.to_s.pluralize.to_sym, module: :devise, class_name: resource ) end + + def self.reconfigure_warden! + Devise.class_variable_set(:@@warden_configured, nil) + Devise.configure_warden! + end + + def self.to_mapping_name(resource) + resource.to_s.underscore.tr('/', '_') + end end require 'graphql_devise/engine' diff --git a/lib/graphql_devise/resource_loader.rb b/lib/graphql_devise/resource_loader.rb index b5a8e4ae..6366ed9e 100644 --- a/lib/graphql_devise/resource_loader.rb +++ b/lib/graphql_devise/resource_loader.rb @@ -10,7 +10,7 @@ def initialize(resource, options = {}, routing = false) end def call(query, mutation) - mapping_name = @resource.to_s.underscore.tr('/', '_').to_sym + mapping_name = GraphqlDevise.to_mapping_name(@resource).to_sym # clean_options responds to all keys defined in GraphqlDevise::MountMethod::SUPPORTED_OPTIONS clean_options = GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call! diff --git a/lib/graphql_devise/schema_plugin.rb b/lib/graphql_devise/schema_plugin.rb index 5adf3b76..f4ad4a91 100644 --- a/lib/graphql_devise/schema_plugin.rb +++ b/lib/graphql_devise/schema_plugin.rb @@ -16,7 +16,7 @@ def initialize(query: nil, mutation: nil, authenticate_default: true, public_int # Must happen on initialize so operations are loaded before the types are added to the schema on GQL < 1.10 load_fields - reconfigure_warden! + GraphqlDevise.reconfigure_warden! end def use(schema_definition) @@ -102,11 +102,6 @@ def authenticate_option(field, trace_data) auth_required.nil? ? @authenticate_default : auth_required end - def reconfigure_warden! - Devise.class_variable_set(:@@warden_configured, nil) - Devise.configure_warden! - end - def load_fields @resource_loaders.each do |resource_loader| raise Error, 'Invalid resource loader instance' unless resource_loader.instance_of?(GraphqlDevise::ResourceLoader) diff --git a/spec/dummy/app/controllers/api/v1/graphql_controller.rb b/spec/dummy/app/controllers/api/v1/graphql_controller.rb index afe9b7af..59cfb743 100644 --- a/spec/dummy/app/controllers/api/v1/graphql_controller.rb +++ b/spec/dummy/app/controllers/api/v1/graphql_controller.rb @@ -5,6 +5,8 @@ module V1 class GraphqlController < ApplicationController include GraphqlDevise::Concerns::SetUserByToken + set_resource_by_model SchemaUser, only: [:controller_auth] + def graphql result = DummySchema.execute(params[:query], **execute_params(params)) @@ -19,6 +21,12 @@ def failing_resource_name render json: DummySchema.execute(params[:query], context: graphql_context([:user, :fail])) end + def controller_auth + result = DummySchema.execute(params[:query], **execute_params(params)) + + render json: result unless performed? + end + private def execute_params(item) diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index cb34c1fe..00a51dfd 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -37,4 +37,5 @@ post '/api/v1/graphql', to: 'api/v1/graphql#graphql' post '/api/v1/interpreter', to: 'api/v1/graphql#interpreter' post '/api/v1/failing', to: 'api/v1/graphql#failing_resource_name' + post '/api/v1/controller_auth', to: 'api/v1/graphql#controller_auth' end diff --git a/spec/graphql/user_queries_spec.rb b/spec/graphql/user_queries_spec.rb index 17be663a..f731fe89 100644 --- a/spec/graphql/user_queries_spec.rb +++ b/spec/graphql/user_queries_spec.rb @@ -78,7 +78,9 @@ let(:query) do <<-GRAPHQL query { - user(id: #{user.id}) { + user( + id: #{user.id} + ) { id email } diff --git a/spec/requests/user_controller_spec.rb b/spec/requests/user_controller_spec.rb index 29fadac5..2ba351ca 100644 --- a/spec/requests/user_controller_spec.rb +++ b/spec/requests/user_controller_spec.rb @@ -42,6 +42,16 @@ GRAPHQL end + context 'when authenticating before using the GQL schema' do + let(:headers) { create(:schema_user).create_new_auth_token } + + before { post_request('/api/v1/controller_auth') } + + it 'allows authentication at the controller level' do + expect(json_response[:data][:privateField]).to eq('Field will always require authentication') + end + end + context 'when using a regular schema' do before { post_request('/api/v1/graphql') } From 82155a844bff08ad7d126b9391bcfed0128139ea Mon Sep 17 00:00:00 2001 From: Mario Celi Date: Sun, 2 May 2021 20:27:05 -0500 Subject: [PATCH 2/4] Stop depending on devise mappings to mount resources --- .../concerns/set_user_by_token.rb | 12 +++------- lib/graphql_devise.rb | 23 +++++++++++-------- .../concerns/controller_methods.rb | 6 ++--- .../mount_method/operation_preparer.rb | 12 +++++----- .../custom_operation_preparer.rb | 10 ++++---- .../default_operation_preparer.rb | 10 ++++---- ...ame_setter.rb => resource_klass_setter.rb} | 8 +++---- lib/graphql_devise/resource_loader.rb | 23 ++++++++++--------- lib/graphql_devise/schema_plugin.rb | 1 - spec/dummy/config/routes.rb | 2 +- spec/requests/user_controller_spec.rb | 18 +++++++++++---- .../mount_method/operation_preparer_spec.rb | 10 ++++---- .../custom_operation_preparer_spec.rb | 10 ++++---- .../default_operation_preparer_spec.rb | 10 ++++---- ..._spec.rb => resource_klass_setter_spec.rb} | 12 +++++----- spec/services/resource_loader_spec.rb | 10 ++++---- 16 files changed, 95 insertions(+), 82 deletions(-) rename lib/graphql_devise/mount_method/operation_preparers/{resource_name_setter.rb => resource_klass_setter.rb} (57%) rename spec/services/mount_method/operation_preparers/{resource_name_setter_spec.rb => resource_klass_setter_spec.rb} (51%) diff --git a/app/controllers/graphql_devise/concerns/set_user_by_token.rb b/app/controllers/graphql_devise/concerns/set_user_by_token.rb index 9c2bb0c3..9c0be45e 100644 --- a/app/controllers/graphql_devise/concerns/set_user_by_token.rb +++ b/app/controllers/graphql_devise/concerns/set_user_by_token.rb @@ -10,9 +10,8 @@ module Concerns class_methods do def set_resource_by_model(models, **kwargs) Array(models).each do |model| - GraphqlDevise.add_mapping(GraphqlDevise.to_mapping_name(model.to_s), model.to_s) + GraphqlDevise.configure_warden_serializer_for_model(model) end - GraphqlDevise.reconfigure_warden! before_action(**kwargs) do authenticate_model(models) @@ -23,19 +22,14 @@ def set_resource_by_model(models, **kwargs) def authenticate_model(models) Array(models).each do |model| set_resource_by_token(model) - return @resource + return @resource if @resource.present? end nil end def resource_class(resource = nil) - return super unless resource.is_a?(Class) - - if (Object.const_defined?('ActiveRecord::Base') && resource < ActiveRecord::Base) || - (Object.const_defined?('Mongoid::Document') && resource < Mongoid::Document) - return resource - end + return resource if resource.respond_to?(:find_by) super end diff --git a/lib/graphql_devise.rb b/lib/graphql_devise.rb index 53eb0739..129cb6f6 100644 --- a/lib/graphql_devise.rb +++ b/lib/graphql_devise.rb @@ -20,12 +20,12 @@ def self.load_schema @schema_loaded = true end - def self.resource_mounted?(mapping_name) - @mounted_resources.include?(mapping_name) + def self.resource_mounted?(model) + @mounted_resources.include?(model) end - def self.mount_resource(mapping_name) - @mounted_resources << mapping_name + def self.mount_resource(model) + @mounted_resources << model end def self.add_mapping(mapping_name, resource) @@ -37,14 +37,19 @@ def self.add_mapping(mapping_name, resource) ) end - def self.reconfigure_warden! - Devise.class_variable_set(:@@warden_configured, nil) - Devise.configure_warden! - end - def self.to_mapping_name(resource) resource.to_s.underscore.tr('/', '_') end + + def self.configure_warden_serializer_for_model(model) + Devise.warden_config.serialize_into_session(to_mapping_name(model)) do |record| + model.serialize_into_session(record) + end + + Devise.warden_config.serialize_from_session(to_mapping_name(model)) do |args| + model.serialize_from_session(*args) + end + end end require 'graphql_devise/engine' diff --git a/lib/graphql_devise/concerns/controller_methods.rb b/lib/graphql_devise/concerns/controller_methods.rb index 382fe850..1ec24e07 100644 --- a/lib/graphql_devise/concerns/controller_methods.rb +++ b/lib/graphql_devise/concerns/controller_methods.rb @@ -40,11 +40,11 @@ def controller end def resource_name - self.class.instance_variable_get(:@resource_name) + GraphqlDevise.to_mapping_name(resource_class) end def resource_class - controller.send(:resource_class, resource_name) + self.class.instance_variable_get(:@resource_klass) end def recoverable_enabled? @@ -60,7 +60,7 @@ def blacklisted_redirect_url?(redirect_url) end def current_resource - @current_resource ||= controller.send(:set_user_by_token, resource_name) + @current_resource ||= controller.send(:set_user_by_token, resource_class) end def client diff --git a/lib/graphql_devise/mount_method/operation_preparer.rb b/lib/graphql_devise/mount_method/operation_preparer.rb index f6e0b493..bee93aa7 100644 --- a/lib/graphql_devise/mount_method/operation_preparer.rb +++ b/lib/graphql_devise/mount_method/operation_preparer.rb @@ -3,17 +3,17 @@ require_relative 'operation_preparers/gql_name_setter' require_relative 'operation_preparers/mutation_field_setter' require_relative 'operation_preparers/resolver_type_setter' -require_relative 'operation_preparers/resource_name_setter' +require_relative 'operation_preparers/resource_klass_setter' require_relative 'operation_preparers/default_operation_preparer' require_relative 'operation_preparers/custom_operation_preparer' module GraphqlDevise module MountMethod class OperationPreparer - def initialize(mapping_name:, selected_operations:, preparer:, custom:, additional_operations:) + def initialize(model:, selected_operations:, preparer:, custom:, additional_operations:) @selected_operations = selected_operations @preparer = preparer - @mapping_name = mapping_name + @model = model @custom = custom @additional_operations = additional_operations end @@ -22,18 +22,18 @@ def call default_operations = OperationPreparers::DefaultOperationPreparer.new( selected_operations: @selected_operations, custom_keys: @custom.keys, - mapping_name: @mapping_name, + model: @model, preparer: @preparer ).call custom_operations = OperationPreparers::CustomOperationPreparer.new( selected_keys: @selected_operations.keys, custom_operations: @custom, - mapping_name: @mapping_name + model: @model ).call additional_operations = @additional_operations.each_with_object({}) do |(action, operation), result| - result[action] = OperationPreparers::ResourceNameSetter.new(@mapping_name).call(operation) + result[action] = OperationPreparers::ResourceKlassSetter.new(@model).call(operation) end default_operations.merge(custom_operations).merge(additional_operations) diff --git a/lib/graphql_devise/mount_method/operation_preparers/custom_operation_preparer.rb b/lib/graphql_devise/mount_method/operation_preparers/custom_operation_preparer.rb index 054ed695..b52782d1 100644 --- a/lib/graphql_devise/mount_method/operation_preparers/custom_operation_preparer.rb +++ b/lib/graphql_devise/mount_method/operation_preparers/custom_operation_preparer.rb @@ -4,19 +4,21 @@ module GraphqlDevise module MountMethod module OperationPreparers class CustomOperationPreparer - def initialize(selected_keys:, custom_operations:, mapping_name:) + def initialize(selected_keys:, custom_operations:, model:) @selected_keys = selected_keys @custom_operations = custom_operations - @mapping_name = mapping_name + @model = model end def call + mapping_name = GraphqlDevise.to_mapping_name(@model) + @custom_operations.slice(*@selected_keys).each_with_object({}) do |(action, operation), result| - mapped_action = "#{@mapping_name}_#{action}" + mapped_action = "#{mapping_name}_#{action}" result[mapped_action.to_sym] = [ OperationPreparers::GqlNameSetter.new(mapped_action), - OperationPreparers::ResourceNameSetter.new(@mapping_name) + OperationPreparers::ResourceKlassSetter.new(@model) ].reduce(operation) { |prepared_operation, preparer| preparer.call(prepared_operation) } end end diff --git a/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb b/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb index 4730ef16..f83bec46 100644 --- a/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb +++ b/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb @@ -4,23 +4,25 @@ module GraphqlDevise module MountMethod module OperationPreparers class DefaultOperationPreparer - def initialize(selected_operations:, custom_keys:, mapping_name:, preparer:) + def initialize(selected_operations:, custom_keys:, model:, preparer:) @selected_operations = selected_operations @custom_keys = custom_keys - @mapping_name = mapping_name + @model = model @preparer = preparer end def call + mapping_name = GraphqlDevise.to_mapping_name(@model) + @selected_operations.except(*@custom_keys).each_with_object({}) do |(action, operation_info), result| - mapped_action = "#{@mapping_name}_#{action}" + mapped_action = "#{mapping_name}_#{action}" operation = operation_info[:klass] options = operation_info.except(:klass) result[mapped_action.to_sym] = [ OperationPreparers::GqlNameSetter.new(mapped_action), @preparer, - OperationPreparers::ResourceNameSetter.new(@mapping_name) + OperationPreparers::ResourceKlassSetter.new(@model) ].reduce(child_class(operation)) do |prepared_operation, preparer| preparer.call(prepared_operation, **options) end diff --git a/lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb b/lib/graphql_devise/mount_method/operation_preparers/resource_klass_setter.rb similarity index 57% rename from lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb rename to lib/graphql_devise/mount_method/operation_preparers/resource_klass_setter.rb index 2d3d6f58..a37db672 100644 --- a/lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb +++ b/lib/graphql_devise/mount_method/operation_preparers/resource_klass_setter.rb @@ -3,13 +3,13 @@ module GraphqlDevise module MountMethod module OperationPreparers - class ResourceNameSetter - def initialize(name) - @name = name + class ResourceKlassSetter + def initialize(klass) + @klass = klass end def call(operation, **) - operation.instance_variable_set(:@resource_name, @name) + operation.instance_variable_set(:@resource_klass, @klass) operation end diff --git a/lib/graphql_devise/resource_loader.rb b/lib/graphql_devise/resource_loader.rb index 6366ed9e..52f0dc32 100644 --- a/lib/graphql_devise/resource_loader.rb +++ b/lib/graphql_devise/resource_loader.rb @@ -10,12 +10,12 @@ def initialize(resource, options = {}, routing = false) end def call(query, mutation) - mapping_name = GraphqlDevise.to_mapping_name(@resource).to_sym - # clean_options responds to all keys defined in GraphqlDevise::MountMethod::SUPPORTED_OPTIONS clean_options = GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call! - return clean_options if GraphqlDevise.resource_mounted?(mapping_name) && @routing + model = @resource.is_a?(String) ? @resource.constantize : @resource + + return clean_options if GraphqlDevise.resource_mounted?(model) && @routing validate_options!(clean_options) @@ -23,7 +23,7 @@ def call(query, mutation) "Types::#{@resource}Type".safe_constantize || GraphqlDevise::Types::AuthenticatableType - prepared_mutations = prepare_mutations(mapping_name, clean_options, authenticatable_type) + prepared_mutations = prepare_mutations(model, clean_options, authenticatable_type) if prepared_mutations.any? && mutation.blank? raise GraphqlDevise::Error, 'You need to provide a mutation type unless all mutations are skipped' @@ -33,7 +33,7 @@ def call(query, mutation) mutation.field(action, mutation: prepared_mutation, authenticate: false) end - prepared_resolvers = prepare_resolvers(mapping_name, clean_options, authenticatable_type) + prepared_resolvers = prepare_resolvers(model, clean_options, authenticatable_type) if prepared_resolvers.any? && query.blank? raise GraphqlDevise::Error, 'You need to provide a query type unless all queries are skipped' @@ -43,17 +43,18 @@ def call(query, mutation) query.field(action, resolver: resolver, authenticate: false) end - GraphqlDevise.add_mapping(mapping_name, @resource) - GraphqlDevise.mount_resource(mapping_name) if @routing + GraphqlDevise.configure_warden_serializer_for_model(model) + GraphqlDevise.add_mapping(GraphqlDevise.to_mapping_name(@resource).to_sym, @resource) + GraphqlDevise.mount_resource(model) if @routing clean_options end private - def prepare_resolvers(mapping_name, clean_options, authenticatable_type) + def prepare_resolvers(model, clean_options, authenticatable_type) GraphqlDevise::MountMethod::OperationPreparer.new( - mapping_name: mapping_name, + model: model, custom: clean_options.operations, additional_operations: clean_options.additional_queries, preparer: GraphqlDevise::MountMethod::OperationPreparers::ResolverTypeSetter.new(authenticatable_type), @@ -63,9 +64,9 @@ def prepare_resolvers(mapping_name, clean_options, authenticatable_type) ).call end - def prepare_mutations(mapping_name, clean_options, authenticatable_type) + def prepare_mutations(model, clean_options, authenticatable_type) GraphqlDevise::MountMethod::OperationPreparer.new( - mapping_name: mapping_name, + model: model, custom: clean_options.operations, additional_operations: clean_options.additional_mutations, preparer: GraphqlDevise::MountMethod::OperationPreparers::MutationFieldSetter.new(authenticatable_type), diff --git a/lib/graphql_devise/schema_plugin.rb b/lib/graphql_devise/schema_plugin.rb index f4ad4a91..fd8fbc1c 100644 --- a/lib/graphql_devise/schema_plugin.rb +++ b/lib/graphql_devise/schema_plugin.rb @@ -16,7 +16,6 @@ def initialize(query: nil, mutation: nil, authenticate_default: true, public_int # Must happen on initialize so operations are loaded before the types are added to the schema on GQL < 1.10 load_fields - GraphqlDevise.reconfigure_warden! end def use(schema_definition) diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 00a51dfd..bc090e35 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -11,7 +11,7 @@ } mount_graphql_devise_for( - 'Admin', + Admin, authenticatable_type: Types::CustomAdminType, skip: [:sign_up, :check_password_token], operations: { diff --git a/spec/requests/user_controller_spec.rb b/spec/requests/user_controller_spec.rb index 2ba351ca..07fbafa1 100644 --- a/spec/requests/user_controller_spec.rb +++ b/spec/requests/user_controller_spec.rb @@ -43,12 +43,22 @@ end context 'when authenticating before using the GQL schema' do - let(:headers) { create(:schema_user).create_new_auth_token } - before { post_request('/api/v1/controller_auth') } - it 'allows authentication at the controller level' do - expect(json_response[:data][:privateField]).to eq('Field will always require authentication') + context 'when user is authenticated' do + let(:headers) { create(:schema_user).create_new_auth_token } + + it 'allows authentication at the controller level' do + expect(json_response[:data][:privateField]).to eq('Field will always require authentication') + end + end + + context 'when user is not authenticated' do + it 'returns a must sign in error' do + expect(json_response[:errors]).to contain_exactly( + hash_including(message: 'privateField field requires authentication', extensions: { code: 'AUTHENTICATION_ERROR' }) + ) + end end end diff --git a/spec/services/mount_method/operation_preparer_spec.rb b/spec/services/mount_method/operation_preparer_spec.rb index 145a18d4..4a8c196c 100644 --- a/spec/services/mount_method/operation_preparer_spec.rb +++ b/spec/services/mount_method/operation_preparer_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'spec_helper' +require 'rails_helper' RSpec.describe GraphqlDevise::MountMethod::OperationPreparer do describe '#call' do subject(:prepared_operations) do described_class.new( - mapping_name: mapping, + model: model, selected_operations: selected, preparer: preparer, custom: custom, @@ -15,14 +15,14 @@ end let(:logout_class) { Class.new(GraphQL::Schema::Resolver) } - let(:mapping) { :user } + let(:model) { User } let(:preparer) { double(:preparer, call: logout_class) } let(:custom) { { login: double(:custom_login, graphql_name: nil) } } let(:additional) { { user_additional: double(:user_additional) } } let(:selected) do { - login: { klass: double(:login_default) }, - logout:{ klass: logout_class } + login: { klass: double(:login_default) }, + logout: { klass: logout_class } } end diff --git a/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb b/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb index 455b2683..da19bb4d 100644 --- a/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +++ b/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true -require 'spec_helper' +require 'rails_helper' RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::CustomOperationPreparer do describe '#call' do - subject(:prepared) { described_class.new(selected_keys: selected_keys, custom_operations: operations, mapping_name: mapping_name).call } + subject(:prepared) { described_class.new(selected_keys: selected_keys, custom_operations: operations, model: model).call } let(:login_operation) { double(:confirm_operation, graphql_name: nil) } let(:logout_operation) { double(:sign_up_operation, graphql_name: nil) } - let(:mapping_name) { :user } + let(:model) { User } let(:operations) { { login: login_operation, logout: logout_operation, invalid: double(:invalid) } } let(:selected_keys) { [:login, :logout, :sign_up, :confirm] } @@ -22,8 +22,8 @@ prepared - expect(login_operation.instance_variable_get(:@resource_name)).to eq(:user) - expect(logout_operation.instance_variable_get(:@resource_name)).to eq(:user) + expect(login_operation.instance_variable_get(:@resource_klass)).to eq(User) + expect(logout_operation.instance_variable_get(:@resource_klass)).to eq(User) end context 'when no selected keys are provided' do diff --git a/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb b/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb index d51bc68c..fc072095 100644 --- a/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +++ b/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true -require 'spec_helper' +require 'rails_helper' RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::DefaultOperationPreparer do describe '#call' do subject(:prepared) { default_preparer.call } - let(:default_preparer) { described_class.new(selected_operations: operations, custom_keys: custom_keys, mapping_name: mapping_name, preparer: preparer) } + let(:default_preparer) { described_class.new(selected_operations: operations, custom_keys: custom_keys, model: model, preparer: preparer) } let(:confirm_operation) { double(:confirm_operation, graphql_name: nil) } let(:sign_up_operation) { double(:sign_up_operation, graphql_name: nil) } let(:login_operation) { double(:confirm_operation, graphql_name: nil) } let(:logout_operation) { double(:sign_up_operation, graphql_name: nil) } - let(:mapping_name) { :user } + let(:model) { User } let(:preparer) { double(:preparer) } let(:custom_keys) { [:login, :logout] } let(:operations) do @@ -46,8 +46,8 @@ prepared - expect(confirm_operation.instance_variable_get(:@resource_name)).to eq(:user) - expect(sign_up_operation.instance_variable_get(:@resource_name)).to eq(:user) + expect(confirm_operation.instance_variable_get(:@resource_klass)).to eq(User) + expect(sign_up_operation.instance_variable_get(:@resource_klass)).to eq(User) end context 'when no custom keys are provided' do diff --git a/spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb b/spec/services/mount_method/operation_preparers/resource_klass_setter_spec.rb similarity index 51% rename from spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb rename to spec/services/mount_method/operation_preparers/resource_klass_setter_spec.rb index 88e0ff05..573f5a3e 100644 --- a/spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb +++ b/spec/services/mount_method/operation_preparers/resource_klass_setter_spec.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require 'spec_helper' +require 'rails_helper' -RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::ResourceNameSetter do +RSpec.describe GraphqlDevise::MountMethod::OperationPreparers::ResourceKlassSetter do describe '#call' do - subject(:prepared_operation) { described_class.new(mapping_name).call(operation) } + subject(:prepared_operation) { described_class.new(model).call(operation) } - let(:operation) { double(:operation) } - let(:mapping_name) { :user } + let(:operation) { double(:operation) } + let(:model) { User } it 'sets a gql name to the operation' do - expect(prepared_operation.instance_variable_get(:@resource_name)).to eq(:user) + expect(prepared_operation.instance_variable_get(:@resource_klass)).to eq(model) end end end diff --git a/spec/services/resource_loader_spec.rb b/spec/services/resource_loader_spec.rb index 2cb7101f..b2798526 100644 --- a/spec/services/resource_loader_spec.rb +++ b/spec/services/resource_loader_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'rails_helper' RSpec.describe GraphqlDevise::ResourceLoader do describe '#call' do @@ -15,8 +15,8 @@ before do allow(GraphqlDevise).to receive(:add_mapping).with(:user, resource) - allow(GraphqlDevise).to receive(:resource_mounted?).with(:user).and_return(mounted) - allow(GraphqlDevise).to receive(:mount_resource).with(:user) + allow(GraphqlDevise).to receive(:resource_mounted?).with(User).and_return(mounted) + allow(GraphqlDevise).to receive(:mount_resource).with(User) end it 'loads operations into the provided types' do @@ -64,13 +64,13 @@ it 'adds mappings' do expect(GraphqlDevise).to receive(:add_mapping).with(:user, resource) - expect(GraphqlDevise).to receive(:mount_resource).with(:user) + expect(GraphqlDevise).to receive(:mount_resource).with(User) loader end context 'when resource was already mounted' do - before { allow(GraphqlDevise).to receive(:resource_mounted?).with(:user).and_return(true) } + before { allow(GraphqlDevise).to receive(:resource_mounted?).with(User).and_return(true) } it 'skips schema loading' do expect(query).not_to receive(:field) From 45bda76bdefb357833b89823552a778bfc31b575 Mon Sep 17 00:00:00 2001 From: Mario Celi Date: Tue, 4 May 2021 21:34:10 -0500 Subject: [PATCH 3/4] Configure warden when gem is included in the model --- .../concerns/additional_controller_methods.rb | 60 ++++++++++++++++++ .../concerns/set_user_by_token.rb | 62 ++----------------- .../concerns/additional_model_methods.rb | 21 +++++++ app/models/graphql_devise/concerns/model.rb | 15 ++--- .../concerns/controller_methods.rb | 2 +- lib/graphql_devise/resource_loader.rb | 1 - .../controllers/api/v1/graphql_controller.rb | 9 ++- 7 files changed, 99 insertions(+), 71 deletions(-) create mode 100644 app/controllers/graphql_devise/concerns/additional_controller_methods.rb create mode 100644 app/models/graphql_devise/concerns/additional_model_methods.rb diff --git a/app/controllers/graphql_devise/concerns/additional_controller_methods.rb b/app/controllers/graphql_devise/concerns/additional_controller_methods.rb new file mode 100644 index 00000000..469a3c20 --- /dev/null +++ b/app/controllers/graphql_devise/concerns/additional_controller_methods.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module GraphqlDevise + module Concerns + module AdditionalControllerMethods + extend ActiveSupport::Concern + + included do + attr_accessor :client_id, :token, :resource + end + + def gql_devise_context(models) + { + current_resource: authenticate_model(models), + controller: self + } + end + + def authenticate_model(models) + Array(models).each do |model| + set_resource_by_token(model) + return @resource if @resource.present? + end + + nil + end + + def resource_class(resource = nil) + return resource if resource.respond_to?(:find_by) + + super + end + + def full_url_without_params + request.base_url + request.path + end + + def set_resource_by_token(resource) + set_user_by_token(resource) + end + + def graphql_context(resource_name) + { + resource_name: resource_name, + controller: self + } + end + + def build_redirect_headers(access_token, client, redirect_header_options = {}) + { + DeviseTokenAuth.headers_names[:"access-token"] => access_token, + DeviseTokenAuth.headers_names[:client] => client, + :config => params[:config], + :client_id => client, + :token => access_token + }.merge(redirect_header_options) + end + end + end +end diff --git a/app/controllers/graphql_devise/concerns/set_user_by_token.rb b/app/controllers/graphql_devise/concerns/set_user_by_token.rb index 9c0be45e..31267959 100644 --- a/app/controllers/graphql_devise/concerns/set_user_by_token.rb +++ b/app/controllers/graphql_devise/concerns/set_user_by_token.rb @@ -2,64 +2,12 @@ module GraphqlDevise module Concerns - SetUserByToken = DeviseTokenAuth::Concerns::SetUserByToken + module SetUserByToken + extend ActiveSupport::Concern - SetUserByToken.module_eval do - attr_accessor :client_id, :token, :resource - - class_methods do - def set_resource_by_model(models, **kwargs) - Array(models).each do |model| - GraphqlDevise.configure_warden_serializer_for_model(model) - end - - before_action(**kwargs) do - authenticate_model(models) - end - end - end - - def authenticate_model(models) - Array(models).each do |model| - set_resource_by_token(model) - return @resource if @resource.present? - end - - nil - end - - def resource_class(resource = nil) - return resource if resource.respond_to?(:find_by) - - super - end - - def full_url_without_params - request.base_url + request.path - end - - def set_resource_by_token(resource) - set_user_by_token(resource) - end - - def graphql_context(resource_name) - context = { - resource_name: resource_name, - controller: self - } - context[:current_resource] = @resource if @resource.present? - - context - end - - def build_redirect_headers(access_token, client, redirect_header_options = {}) - { - DeviseTokenAuth.headers_names[:"access-token"] => access_token, - DeviseTokenAuth.headers_names[:client] => client, - :config => params[:config], - :client_id => client, - :token => access_token - }.merge(redirect_header_options) + included do + include DeviseTokenAuth::Concerns::SetUserByToken + include GraphqlDevise::Concerns::AdditionalControllerMethods end end end diff --git a/app/models/graphql_devise/concerns/additional_model_methods.rb b/app/models/graphql_devise/concerns/additional_model_methods.rb new file mode 100644 index 00000000..c9a5734d --- /dev/null +++ b/app/models/graphql_devise/concerns/additional_model_methods.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'graphql_devise/model/with_email_updater' + +module GraphqlDevise + module Concerns + module AdditionalModelMethods + extend ActiveSupport::Concern + + class_methods do + def reconfirmable + devise_modules.include?(:confirmable) && column_names.include?('unconfirmed_email') + end + end + + def update_with_email(attributes = {}) + GraphqlDevise::Model::WithEmailUpdater.new(self, attributes).call + end + end + end +end diff --git a/app/models/graphql_devise/concerns/model.rb b/app/models/graphql_devise/concerns/model.rb index 287cc5c4..28b70e2a 100644 --- a/app/models/graphql_devise/concerns/model.rb +++ b/app/models/graphql_devise/concerns/model.rb @@ -4,17 +4,14 @@ module GraphqlDevise module Concerns - Model = DeviseTokenAuth::Concerns::User + module Model + extend ActiveSupport::Concern - Model.module_eval do - class_methods do - def reconfirmable - devise_modules.include?(:confirmable) && column_names.include?('unconfirmed_email') - end - end + included do + include DeviseTokenAuth::Concerns::User + include GraphqlDevise::Concerns::AdditionalModelMethods - def update_with_email(attributes = {}) - GraphqlDevise::Model::WithEmailUpdater.new(self, attributes).call + GraphqlDevise.configure_warden_serializer_for_model(self) end end end diff --git a/lib/graphql_devise/concerns/controller_methods.rb b/lib/graphql_devise/concerns/controller_methods.rb index 1ec24e07..bb5a9174 100644 --- a/lib/graphql_devise/concerns/controller_methods.rb +++ b/lib/graphql_devise/concerns/controller_methods.rb @@ -60,7 +60,7 @@ def blacklisted_redirect_url?(redirect_url) end def current_resource - @current_resource ||= controller.send(:set_user_by_token, resource_class) + @current_resource ||= controller.send(:set_resource_by_token, resource_class) end def client diff --git a/lib/graphql_devise/resource_loader.rb b/lib/graphql_devise/resource_loader.rb index 52f0dc32..e4a4f25f 100644 --- a/lib/graphql_devise/resource_loader.rb +++ b/lib/graphql_devise/resource_loader.rb @@ -43,7 +43,6 @@ def call(query, mutation) query.field(action, resolver: resolver, authenticate: false) end - GraphqlDevise.configure_warden_serializer_for_model(model) GraphqlDevise.add_mapping(GraphqlDevise.to_mapping_name(@resource).to_sym, @resource) GraphqlDevise.mount_resource(model) if @routing diff --git a/spec/dummy/app/controllers/api/v1/graphql_controller.rb b/spec/dummy/app/controllers/api/v1/graphql_controller.rb index 59cfb743..c10abd4f 100644 --- a/spec/dummy/app/controllers/api/v1/graphql_controller.rb +++ b/spec/dummy/app/controllers/api/v1/graphql_controller.rb @@ -5,8 +5,6 @@ module V1 class GraphqlController < ApplicationController include GraphqlDevise::Concerns::SetUserByToken - set_resource_by_model SchemaUser, only: [:controller_auth] - def graphql result = DummySchema.execute(params[:query], **execute_params(params)) @@ -22,7 +20,12 @@ def failing_resource_name end def controller_auth - result = DummySchema.execute(params[:query], **execute_params(params)) + result = DummySchema.execute( + params[:query], + operation_name: params[:operationName], + variables: ensure_hash(params[:variables]), + context: gql_devise_context(SchemaUser) + ) render json: result unless performed? end From 2155f5d2849cfc4f2a0267a9563d575685c06baa Mon Sep 17 00:00:00 2001 From: Mario Celi Date: Tue, 4 May 2021 22:46:57 -0500 Subject: [PATCH 4/4] Add comments for resource_class and resource_loader --- .../graphql_devise/concerns/additional_controller_methods.rb | 1 + lib/graphql_devise/resource_loader.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/controllers/graphql_devise/concerns/additional_controller_methods.rb b/app/controllers/graphql_devise/concerns/additional_controller_methods.rb index 469a3c20..5171f1c2 100644 --- a/app/controllers/graphql_devise/concerns/additional_controller_methods.rb +++ b/app/controllers/graphql_devise/concerns/additional_controller_methods.rb @@ -26,6 +26,7 @@ def authenticate_model(models) end def resource_class(resource = nil) + # Return the resource class instead of looking for a Devise mapping if resource is already a resource class return resource if resource.respond_to?(:find_by) super diff --git a/lib/graphql_devise/resource_loader.rb b/lib/graphql_devise/resource_loader.rb index e4a4f25f..b16a6774 100644 --- a/lib/graphql_devise/resource_loader.rb +++ b/lib/graphql_devise/resource_loader.rb @@ -15,6 +15,7 @@ def call(query, mutation) model = @resource.is_a?(String) ? @resource.constantize : @resource + # Necesary when mounting a resource via route file as Devise forces the reloading of routes return clean_options if GraphqlDevise.resource_mounted?(model) && @routing validate_options!(clean_options)