graphql_devise 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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