graphql_devise 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +36 -18
  5. data/CHANGELOG.md +56 -0
  6. data/README.md +274 -35
  7. data/app/controllers/graphql_devise/application_controller.rb +4 -1
  8. data/app/controllers/graphql_devise/concerns/set_user_by_token.rb +23 -0
  9. data/app/controllers/graphql_devise/graphql_controller.rb +2 -0
  10. data/app/helpers/graphql_devise/mailer_helper.rb +2 -2
  11. data/config/routes.rb +18 -0
  12. data/graphql_devise.gemspec +3 -3
  13. data/lib/generators/graphql_devise/install_generator.rb +63 -30
  14. data/lib/graphql_devise.rb +32 -0
  15. data/lib/graphql_devise/concerns/controller_methods.rb +23 -0
  16. data/lib/graphql_devise/mount_method/operation_preparer.rb +2 -2
  17. data/lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb +1 -1
  18. data/lib/graphql_devise/mutations/login.rb +4 -1
  19. data/lib/graphql_devise/mutations/resend_confirmation.rb +4 -1
  20. data/lib/graphql_devise/mutations/send_password_reset.rb +3 -2
  21. data/lib/graphql_devise/mutations/sign_up.rb +1 -9
  22. data/lib/graphql_devise/rails/routes.rb +5 -76
  23. data/lib/graphql_devise/resource_loader.rb +87 -0
  24. data/{app/graphql → lib}/graphql_devise/schema.rb +0 -1
  25. data/lib/graphql_devise/schema_plugin.rb +87 -0
  26. data/lib/graphql_devise/version.rb +1 -1
  27. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +11 -2
  28. data/spec/dummy/app/controllers/application_controller.rb +1 -0
  29. data/spec/dummy/app/graphql/dummy_schema.rb +9 -0
  30. data/spec/dummy/app/graphql/interpreter_schema.rb +9 -0
  31. data/spec/dummy/app/graphql/types/mutation_type.rb +1 -1
  32. data/spec/dummy/app/graphql/types/query_type.rb +10 -0
  33. data/spec/dummy/config/environments/test.rb +1 -1
  34. data/spec/dummy/config/routes.rb +1 -0
  35. data/spec/generators/graphql_devise/install_generator_spec.rb +62 -30
  36. data/spec/rails_helper.rb +4 -1
  37. data/spec/requests/graphql_controller_spec.rb +80 -0
  38. data/spec/requests/mutations/login_spec.rb +14 -2
  39. data/spec/requests/mutations/resend_confirmation_spec.rb +24 -9
  40. data/spec/requests/mutations/send_password_reset_spec.rb +9 -1
  41. data/spec/requests/mutations/sign_up_spec.rb +10 -0
  42. data/spec/requests/user_controller_spec.rb +180 -24
  43. data/spec/services/mount_method/operation_preparer_spec.rb +2 -2
  44. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +1 -1
  45. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +1 -1
  46. data/spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb +1 -1
  47. data/spec/services/resource_loader_spec.rb +82 -0
  48. data/spec/services/schema_plugin_spec.rb +26 -0
  49. data/spec/spec_helper.rb +1 -1
  50. metadata +33 -8
  51. data/spec/support/generators/file_helpers.rb +0 -12
@@ -1,4 +1,7 @@
1
1
  module GraphqlDevise
2
- class ApplicationController < DeviseTokenAuth::ApplicationController
2
+ ApplicationController = if Rails::VERSION::MAJOR >= 5
3
+ Class.new(ActionController::API)
4
+ else
5
+ Class.new(ActionController::Base)
3
6
  end
4
7
  end
@@ -1,5 +1,28 @@
1
1
  module GraphqlDevise
2
2
  module Concerns
3
3
  SetUserByToken = DeviseTokenAuth::Concerns::SetUserByToken
4
+
5
+ SetUserByToken.module_eval do
6
+ attr_accessor :client_id, :token, :resource
7
+
8
+ alias_method :set_resource_by_token, :set_user_by_token
9
+
10
+ def graphql_context
11
+ {
12
+ current_resource: @resource,
13
+ controller: self
14
+ }
15
+ end
16
+
17
+ def build_redirect_headers(access_token, client, redirect_header_options = {})
18
+ {
19
+ DeviseTokenAuth.headers_names[:"access-token"] => access_token,
20
+ DeviseTokenAuth.headers_names[:client] => client,
21
+ :config => params[:config],
22
+ :client_id => client,
23
+ :token => access_token
24
+ }.merge(redirect_header_options)
25
+ end
26
+ end
4
27
  end
5
28
  end
@@ -2,6 +2,8 @@ require_dependency 'graphql_devise/application_controller'
2
2
 
3
3
  module GraphqlDevise
4
4
  class GraphqlController < ApplicationController
5
+ include GraphqlDevise::Concerns::SetUserByToken
6
+
5
7
  def auth
6
8
  result = if params[:_json]
7
9
  GraphqlDevise::Schema.multiplex(
@@ -1,7 +1,7 @@
1
1
  module GraphqlDevise
2
2
  module MailerHelper
3
3
  def confirmation_query(resource_name:, token:, redirect_url:)
4
- name = "#{resource_name.camelize(:lower)}ConfirmAccount"
4
+ name = "#{resource_name.underscore.tr('/', '_').camelize(:lower)}ConfirmAccount"
5
5
  raw = <<-GRAPHQL
6
6
  query($token:String!,$redirectUrl:String!){
7
7
  #{name}(confirmationToken:$token,redirectUrl:$redirectUrl){
@@ -17,7 +17,7 @@ module GraphqlDevise
17
17
  end
18
18
 
19
19
  def password_reset_query(token:, redirect_url:, resource_name:)
20
- name = "#{resource_name.camelize(:lower)}CheckPasswordToken"
20
+ name = "#{resource_name.underscore.tr('/', '_').camelize(:lower)}CheckPasswordToken"
21
21
  raw = <<-GRAPHQL
22
22
  query($token:String!,$redirectUrl:String!){
23
23
  #{name}(resetPasswordToken:$token,redirectUrl:$redirectUrl){
@@ -0,0 +1,18 @@
1
+ GraphqlDevise::Engine.routes.draw do
2
+ # Required as Devise forces routes to reload on eager_load
3
+ unless GraphqlDevise.schema_loaded?
4
+ if GraphqlDevise::Types::QueryType.fields.blank?
5
+ GraphqlDevise::Types::QueryType.field(:dummy, resolver: GraphqlDevise::Resolvers::Dummy)
6
+ end
7
+
8
+ if GraphqlDevise::Types::MutationType.fields.present?
9
+ GraphqlDevise::Schema.mutation(GraphqlDevise::Types::MutationType)
10
+ end
11
+
12
+ GraphqlDevise::Schema.query(GraphqlDevise::Types::QueryType)
13
+
14
+ GraphqlDevise.load_schema
15
+
16
+ Devise.mailer.helper(GraphqlDevise::MailerHelper)
17
+ end
18
+ end
@@ -25,9 +25,9 @@ Gem::Specification.new do |spec|
25
25
 
26
26
  spec.required_ruby_version = '>= 2.2.0'
27
27
 
28
- spec.add_dependency 'devise_token_auth', '>= 0.1.43'
29
- spec.add_dependency 'graphql', '>= 1.8'
30
- spec.add_dependency 'rails', '>= 4.2'
28
+ spec.add_dependency 'devise_token_auth', '>= 0.1.43', '< 2.0'
29
+ spec.add_dependency 'graphql', '>= 1.8', '< 1.11.0'
30
+ spec.add_dependency 'rails', '>= 4.2', '< 6.2'
31
31
 
32
32
  spec.add_development_dependency 'appraisal'
33
33
  spec.add_development_dependency 'coveralls'
@@ -5,62 +5,95 @@ module GraphqlDevise
5
5
  argument :user_class, type: :string, default: 'User'
6
6
  argument :mount_path, type: :string, default: 'auth'
7
7
 
8
+ class_option :mount, type: :string, default: 'separate_route'
9
+
8
10
  def execute_devise_installer
9
11
  generate 'devise:install'
10
12
  end
11
13
 
12
14
  def execute_dta_installer
15
+ # Necessary in case of a re-run of the generator, for DTA to detect concerns already included
16
+ if File.exist?(File.expand_path("app/models/#{user_class.underscore}.rb", destination_root))
17
+ gsub_file(
18
+ "app/models/#{user_class.underscore}.rb",
19
+ 'GraphqlDevise::Concerns::Model',
20
+ 'DeviseTokenAuth::Concerns::User'
21
+ )
22
+ end
23
+ gsub_file(
24
+ 'app/controllers/application_controller.rb',
25
+ 'GraphqlDevise::Concerns::SetUserByToken',
26
+ 'DeviseTokenAuth::Concerns::SetUserByToken'
27
+ )
28
+
13
29
  generate 'devise_token_auth:install', "#{user_class} #{mount_path}"
14
30
  end
15
31
 
16
32
  def mount_resource_route
17
33
  routes_file = 'config/routes.rb'
18
- routes_path = File.join(destination_root, routes_file)
19
- gem_helper = 'mount_graphql_devise_for'
20
- gem_route = "#{gem_helper} '#{user_class}', at: '#{mount_path}'"
21
34
  dta_route = "mount_devise_token_auth_for '#{user_class}', at: '#{mount_path}'"
22
- file_start = 'Rails.application.routes.draw do'
23
35
 
24
- if File.exist?(routes_path)
25
- current_route = parse_file_for_line(routes_path, gem_route)
36
+ if options['mount'] != 'separate_route'
37
+ gsub_file(routes_file, /^\s+#{Regexp.escape(dta_route + "\n")}/i, '')
38
+ else
39
+ gem_route = "mount_graphql_devise_for '#{user_class}', at: '#{mount_path}'"
40
+
41
+ if file_contains_str?(routes_file, gem_route)
42
+ gsub_file(routes_file, /^\s+#{Regexp.escape(dta_route + "\n")}/i, '')
26
43
 
27
- if current_route.present?
28
44
  say_status('skipped', "Routes already exist for #{user_class} at #{mount_path}")
29
45
  else
30
- current_dta_route = parse_file_for_line(routes_path, dta_route)
31
-
32
- if current_dta_route.present?
33
- replace_line(routes_path, dta_route, gem_route)
34
- else
35
- insert_text_after_line(routes_path, file_start, gem_route)
36
- end
46
+ gsub_file(routes_file, /#{Regexp.escape(dta_route)}/i, gem_route)
37
47
  end
38
- else
39
- say_status('skipped', "#{routes_file} not found. Add \"#{gem_route}\" to your routes file.")
40
48
  end
41
49
  end
42
50
 
43
- private
51
+ def replace_model_concern
52
+ gsub_file(
53
+ "app/models/#{user_class.underscore}.rb",
54
+ /^\s+include DeviseTokenAuth::Concerns::User/,
55
+ ' include GraphqlDevise::Concerns::Model'
56
+ )
57
+ end
44
58
 
45
- def insert_text_after_line(filename, line, str)
46
- gsub_file(filename, /(#{Regexp.escape(line)})/mi) do |match|
47
- "#{match}\n #{str}"
48
- end
59
+ def replace_controller_concern
60
+ gsub_file(
61
+ 'app/controllers/application_controller.rb',
62
+ /^\s+include DeviseTokenAuth::Concerns::SetUserByToken/,
63
+ ' include GraphqlDevise::Concerns::SetUserByToken'
64
+ )
49
65
  end
50
66
 
51
- def replace_line(filename, old_line, new_line)
52
- gsub_file(filename, /(#{Regexp.escape(old_line)})/mi, " #{new_line}")
67
+ def set_change_headers_on_each_request_false
68
+ gsub_file(
69
+ 'config/initializers/devise_token_auth.rb',
70
+ '# config.change_headers_on_each_request = true',
71
+ 'config.change_headers_on_each_request = false'
72
+ )
53
73
  end
54
74
 
55
- def parse_file_for_line(filename, str)
56
- match = false
75
+ def mount_in_schema
76
+ return if options['mount'] == 'separate_route'
57
77
 
58
- File.open(filename) do |f|
59
- f.each_line do |line|
60
- match = line if line =~ /(#{Regexp.escape(str)})/mi
61
- end
78
+ inject_into_file "app/graphql/#{options['mount'].underscore}.rb", after: "< GraphQL::Schema\n" do
79
+ <<-RUBY
80
+ use GraphqlDevise::SchemaPlugin.new(
81
+ query: Types::QueryType,
82
+ mutation: Types::MutationType,
83
+ resource_loaders: [
84
+ GraphqlDevise::ResourceLoader.new('#{user_class}'),
85
+ ]
86
+ )
87
+ RUBY
62
88
  end
63
- match
89
+ end
90
+
91
+ private
92
+
93
+ def file_contains_str?(filename, regex_str)
94
+ path = File.join(destination_root, filename)
95
+
96
+ File.read(path) =~ /(#{Regexp.escape(regex_str)})/i
64
97
  end
65
98
  end
66
99
  end
@@ -6,9 +6,38 @@ module GraphqlDevise
6
6
  class Error < StandardError; end
7
7
 
8
8
  class InvalidMountOptionsError < GraphqlDevise::Error; end
9
+
10
+ @schema_loaded = false
11
+ @mounted_resources = []
12
+
13
+ def self.schema_loaded?
14
+ @schema_loaded
15
+ end
16
+
17
+ def self.load_schema
18
+ @schema_loaded = true
19
+ end
20
+
21
+ def self.resource_mounted?(mapping_name)
22
+ @mounted_resources.include?(mapping_name)
23
+ end
24
+
25
+ def self.mount_resource(mapping_name)
26
+ @mounted_resources << mapping_name
27
+ end
28
+
29
+ def self.add_mapping(mapping_name, resource)
30
+ return if Devise.mappings.key?(mapping_name)
31
+
32
+ Devise.add_mapping(
33
+ mapping_name.to_s.pluralize.to_sym,
34
+ module: :devise, class_name: resource
35
+ )
36
+ end
9
37
  end
10
38
 
11
39
  require 'graphql_devise/concerns/controller_methods'
40
+ require 'graphql_devise/schema'
12
41
  require 'graphql_devise/types/authenticatable_type'
13
42
  require 'graphql_devise/types/credential_type'
14
43
  require 'graphql_devise/types/mutation_type'
@@ -27,3 +56,6 @@ require 'graphql_devise/mount_method/option_sanitizer'
27
56
  require 'graphql_devise/mount_method/options_validator'
28
57
  require 'graphql_devise/mount_method/operation_preparer'
29
58
  require 'graphql_devise/mount_method/operation_sanitizer'
59
+
60
+ require 'graphql_devise/resource_loader'
61
+ require 'graphql_devise/schema_plugin'
@@ -86,6 +86,29 @@ module GraphqlDevise
86
86
  redirect_header_options
87
87
  )
88
88
  end
89
+
90
+ def find_resource(field, value)
91
+ if resource_class.try(:connection_config).try(:[], :adapter).try(:include?, 'mysql')
92
+ # fix for mysql default case insensitivity
93
+ resource_class.where("BINARY #{field} = ? AND provider= ?", value, provider).first
94
+ elsif Gem::Version.new(DeviseTokenAuth::VERSION) < Gem::Version.new('1.1.0')
95
+ resource_class.find_by(field => value, :provider => provider)
96
+ else
97
+ resource_class.dta_find_by(field => value, :provider => provider)
98
+ end
99
+ end
100
+
101
+ def get_case_insensitive_field(field, value)
102
+ if resource_class.case_insensitive_keys.include?(field)
103
+ value.downcase
104
+ else
105
+ value
106
+ end
107
+ end
108
+
109
+ def provider
110
+ :email
111
+ end
89
112
  end
90
113
  end
91
114
  end
@@ -8,10 +8,10 @@ require_relative 'operation_preparers/custom_operation_preparer'
8
8
  module GraphqlDevise
9
9
  module MountMethod
10
10
  class OperationPreparer
11
- def initialize(resource:, selected_operations:, preparer:, custom:, additional_operations:)
11
+ def initialize(mapping_name:, selected_operations:, preparer:, custom:, additional_operations:)
12
12
  @selected_operations = selected_operations
13
13
  @preparer = preparer
14
- @mapping_name = resource.underscore.tr('/', '_')
14
+ @mapping_name = mapping_name
15
15
  @custom = custom
16
16
  @additional_operations = additional_operations
17
17
  end
@@ -7,7 +7,7 @@ module GraphqlDevise
7
7
  end
8
8
 
9
9
  def call(operation)
10
- operation.instance_variable_set(:@resource_name, @name.to_sym)
10
+ operation.instance_variable_set(:@resource_name, @name)
11
11
 
12
12
  operation
13
13
  end
@@ -7,7 +7,10 @@ module GraphqlDevise
7
7
  field :credentials, GraphqlDevise::Types::CredentialType, null: false
8
8
 
9
9
  def resolve(email:, password:)
10
- resource = resource_class.find_by(email: email)
10
+ resource = find_resource(
11
+ :email,
12
+ get_case_insensitive_field(:email, email)
13
+ )
11
14
 
12
15
  if resource && active_for_authentication?(resource)
13
16
  if invalid_for_authentication?(resource, password)
@@ -7,7 +7,10 @@ module GraphqlDevise
7
7
  field :message, String, null: false
8
8
 
9
9
  def resolve(email:, redirect_url:)
10
- resource = controller.find_resource(:uid, email)
10
+ resource = find_resource(
11
+ :email,
12
+ get_case_insensitive_field(:email, email)
13
+ )
11
14
 
12
15
  if resource
13
16
  yield resource if block_given?
@@ -1,14 +1,15 @@
1
1
  module GraphqlDevise
2
2
  module Mutations
3
3
  class SendPasswordReset < Base
4
- argument :email, String, required: true, prepare: ->(email, _) { email.downcase }
4
+ argument :email, String, required: true
5
5
  argument :redirect_url, String, required: true
6
6
 
7
7
  def resolve(email:, redirect_url:)
8
- resource = controller.find_resource(:uid, email)
8
+ resource = find_resource(:email, get_case_insensitive_field(:email, email))
9
9
 
10
10
  if resource
11
11
  yield resource if block_given?
12
+
12
13
  resource.send_reset_password_instructions(
13
14
  email: email,
14
15
  provider: 'email',
@@ -35,7 +35,7 @@ module GraphqlDevise
35
35
 
36
36
  { authenticatable: resource }
37
37
  else
38
- clean_up_passwords(resource)
38
+ resource.try(:clean_up_passwords)
39
39
  raise_user_error_list(
40
40
  I18n.t('graphql_devise.registration_failed'),
41
41
  errors: resource.errors.full_messages
@@ -48,14 +48,6 @@ module GraphqlDevise
48
48
  def build_resource(attrs)
49
49
  resource_class.new(attrs)
50
50
  end
51
-
52
- def provider
53
- :email
54
- end
55
-
56
- def clean_up_passwords(resource)
57
- controller.send(:clean_up_passwords, resource)
58
- end
59
51
  end
60
52
  end
61
53
  end
@@ -1,84 +1,13 @@
1
1
  module ActionDispatch::Routing
2
2
  class Mapper
3
- DEVISE_OPERATIONS = [
4
- :sessions,
5
- :registrations,
6
- :passwords,
7
- :confirmations,
8
- :omniauth_callbacks,
9
- :unlocks,
10
- :invitations
11
- ].freeze
12
-
13
3
  def mount_graphql_devise_for(resource, options = {})
14
- default_operations = GraphqlDevise::DefaultOperations::MUTATIONS.merge(GraphqlDevise::DefaultOperations::QUERIES)
15
-
16
- # clean_options responds to all keys defined in GraphqlDevise::MountMethod::SUPPORTED_OPTIONS
17
- clean_options = GraphqlDevise::MountMethod::OptionSanitizer.new(options).call!
18
-
19
- GraphqlDevise::MountMethod::OptionsValidator.new(
20
- [
21
- GraphqlDevise::MountMethod::OptionValidators::SkipOnlyValidator.new(options: clean_options),
22
- GraphqlDevise::MountMethod::OptionValidators::ProvidedOperationsValidator.new(
23
- options: clean_options, supported_operations: default_operations
24
- )
25
- ]
26
- ).validate!
27
-
28
- authenticatable_type = clean_options.authenticatable_type.presence ||
29
- "Types::#{resource}Type".safe_constantize ||
30
- GraphqlDevise::Types::AuthenticatableType
31
-
32
- devise_for(
33
- resource.pluralize.underscore.tr('/', '_').to_sym,
34
- module: :devise,
35
- class_name: resource,
36
- skip: DEVISE_OPERATIONS
4
+ clean_options = GraphqlDevise::ResourceLoader.new(resource, options, true).call(
5
+ GraphqlDevise::Types::QueryType,
6
+ GraphqlDevise::Types::MutationType
37
7
  )
38
8
 
39
- prepared_mutations = GraphqlDevise::MountMethod::OperationPreparer.new(
40
- resource: resource,
41
- custom: clean_options.operations,
42
- additional_operations: clean_options.additional_mutations,
43
- preparer: GraphqlDevise::MountMethod::OperationPreparers::MutationFieldSetter.new(authenticatable_type),
44
- selected_operations: GraphqlDevise::MountMethod::OperationSanitizer.call(
45
- default: GraphqlDevise::DefaultOperations::MUTATIONS, only: clean_options.only, skipped: clean_options.skip
46
- )
47
- ).call
48
-
49
- prepared_mutations.each do |action, mutation|
50
- GraphqlDevise::Types::MutationType.field(action, mutation: mutation)
51
- end
52
-
53
- if prepared_mutations.present? &&
54
- (Gem::Version.new(GraphQL::VERSION) < Gem::Version.new('1.10.0') || GraphqlDevise::Schema.mutation.nil?)
55
- GraphqlDevise::Schema.mutation(GraphqlDevise::Types::MutationType)
56
- end
57
-
58
- prepared_queries = GraphqlDevise::MountMethod::OperationPreparer.new(
59
- resource: resource,
60
- custom: clean_options.operations,
61
- additional_operations: clean_options.additional_queries,
62
- preparer: GraphqlDevise::MountMethod::OperationPreparers::ResolverTypeSetter.new(authenticatable_type),
63
- selected_operations: GraphqlDevise::MountMethod::OperationSanitizer.call(
64
- default: GraphqlDevise::DefaultOperations::QUERIES, only: clean_options.only, skipped: clean_options.skip
65
- )
66
- ).call
67
-
68
- prepared_queries.each do |action, resolver|
69
- GraphqlDevise::Types::QueryType.field(action, resolver: resolver)
70
- end
71
-
72
- if prepared_queries.blank? && GraphqlDevise::Types::QueryType.fields.blank?
73
- GraphqlDevise::Types::QueryType.field(:dummy, resolver: GraphqlDevise::Resolvers::Dummy)
74
- end
75
-
76
- Devise.mailer.helper(GraphqlDevise::MailerHelper)
77
-
78
- devise_scope resource.underscore.tr('/', '_').to_sym do
79
- post clean_options.at, to: 'graphql_devise/graphql#auth'
80
- get clean_options.at, to: 'graphql_devise/graphql#auth'
81
- end
9
+ post clean_options.at, to: 'graphql_devise/graphql#auth'
10
+ get clean_options.at, to: 'graphql_devise/graphql#auth'
82
11
  end
83
12
  end
84
13
  end