graphql_devise 0.11.1 → 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +11 -3
  5. data/CHANGELOG.md +43 -0
  6. data/README.md +191 -32
  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/locales/en.yml +1 -0
  12. data/config/routes.rb +14 -7
  13. data/graphql_devise.gemspec +6 -4
  14. data/lib/generators/graphql_devise/install_generator.rb +63 -30
  15. data/lib/graphql_devise.rb +39 -6
  16. data/lib/graphql_devise/default_operations/mutations.rb +6 -6
  17. data/lib/graphql_devise/default_operations/resolvers.rb +2 -2
  18. data/lib/graphql_devise/errors/authentication_error.rb +7 -0
  19. data/lib/graphql_devise/{detailed_user_error.rb → errors/detailed_user_error.rb} +1 -1
  20. data/lib/graphql_devise/errors/error_codes.rb +6 -0
  21. data/lib/graphql_devise/errors/execution_error.rb +4 -0
  22. data/lib/graphql_devise/{user_error.rb → errors/user_error.rb} +1 -1
  23. data/lib/graphql_devise/mount_method/operation_preparer.rb +2 -2
  24. data/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb +6 -2
  25. data/lib/graphql_devise/mount_method/operation_preparers/gql_name_setter.rb +1 -1
  26. data/lib/graphql_devise/mount_method/operation_preparers/mutation_field_setter.rb +3 -2
  27. data/lib/graphql_devise/mount_method/operation_preparers/resolver_type_setter.rb +1 -1
  28. data/lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb +2 -2
  29. data/lib/graphql_devise/mutations/resend_confirmation.rb +1 -4
  30. data/lib/graphql_devise/mutations/send_password_reset.rb +3 -1
  31. data/lib/graphql_devise/mutations/sign_up.rb +1 -5
  32. data/lib/graphql_devise/rails/routes.rb +5 -67
  33. data/lib/graphql_devise/resource_loader.rb +87 -0
  34. data/lib/graphql_devise/schema_plugin.rb +87 -0
  35. data/lib/graphql_devise/version.rb +1 -1
  36. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +11 -2
  37. data/spec/dummy/app/controllers/application_controller.rb +1 -0
  38. data/spec/dummy/app/graphql/dummy_schema.rb +9 -0
  39. data/spec/dummy/app/graphql/interpreter_schema.rb +9 -0
  40. data/spec/dummy/app/graphql/types/mutation_type.rb +1 -1
  41. data/spec/dummy/app/graphql/types/query_type.rb +10 -0
  42. data/spec/dummy/config/environments/test.rb +1 -1
  43. data/spec/dummy/config/routes.rb +1 -0
  44. data/spec/generators/graphql_devise/install_generator_spec.rb +62 -30
  45. data/spec/rails_helper.rb +4 -1
  46. data/spec/requests/graphql_controller_spec.rb +80 -0
  47. data/spec/requests/mutations/resend_confirmation_spec.rb +2 -14
  48. data/spec/requests/mutations/send_password_reset_spec.rb +8 -3
  49. data/spec/requests/user_controller_spec.rb +180 -24
  50. data/spec/services/mount_method/operation_preparer_spec.rb +8 -3
  51. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +1 -1
  52. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +15 -8
  53. data/spec/services/mount_method/operation_preparers/mutation_field_setter_spec.rb +18 -4
  54. data/spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb +1 -1
  55. data/spec/services/resource_loader_spec.rb +82 -0
  56. data/spec/services/schema_plugin_spec.rb +26 -0
  57. data/spec/spec_helper.rb +1 -1
  58. metadata +107 -89
  59. data/lib/graphql_devise/error_codes.rb +0 -5
  60. 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){
@@ -14,6 +14,7 @@ en:
14
14
  password_not_required: "This account does not require a password. Sign in using your '%{provider}' account instead."
15
15
  reset_token_not_found: "No user found for the specified reset token."
16
16
  reset_token_expired: "Reset password token is no longer valid."
17
+ send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
17
18
  sessions:
18
19
  bad_credentials: "Invalid login credentials. Please try again."
19
20
  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"
@@ -1,11 +1,18 @@
1
1
  GraphqlDevise::Engine.routes.draw do
2
- if GraphqlDevise::Types::QueryType.fields.blank?
3
- GraphqlDevise::Types::QueryType.field(:dummy, resolver: GraphqlDevise::Resolvers::Dummy)
4
- end
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
5
7
 
6
- if GraphqlDevise::Types::MutationType.fields.present?
7
- GraphqlDevise::Schema.mutation(GraphqlDevise::Types::MutationType)
8
- end
8
+ if GraphqlDevise::Types::MutationType.fields.present?
9
+ GraphqlDevise::Schema.mutation(GraphqlDevise::Types::MutationType)
10
+ end
11
+
12
+ GraphqlDevise::Schema.query(GraphqlDevise::Types::QueryType)
9
13
 
10
- GraphqlDevise::Schema.query(GraphqlDevise::Types::QueryType)
14
+ GraphqlDevise.load_schema
15
+
16
+ Devise.mailer.helper(GraphqlDevise::MailerHelper)
17
+ end
11
18
  end
@@ -21,13 +21,15 @@ Gem::Specification.new do |spec|
21
21
  spec.bindir = 'exe'
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ['lib']
24
- spec.test_files = Dir['spec/**/*']
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
25
27
 
26
28
  spec.required_ruby_version = '>= 2.2.0'
27
29
 
28
- spec.add_dependency 'devise_token_auth', '>= 0.1.43'
29
- spec.add_dependency 'graphql', '>= 1.8'
30
- spec.add_dependency 'rails', '>= 4.2'
30
+ spec.add_dependency 'devise_token_auth', '>= 0.1.43', '< 2.0'
31
+ spec.add_dependency 'graphql', '>= 1.8', '< 1.11.0'
32
+ spec.add_dependency 'rails', '>= 4.2', '< 6.2'
31
33
 
32
34
  spec.add_development_dependency 'appraisal'
33
35
  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,8 +6,44 @@ 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
 
39
+ require 'graphql_devise/engine'
40
+ require 'graphql_devise/version'
41
+ require 'graphql_devise/errors/error_codes'
42
+ require 'graphql_devise/errors/execution_error'
43
+ require 'graphql_devise/errors/user_error'
44
+ require 'graphql_devise/errors/authentication_error'
45
+ require 'graphql_devise/errors/detailed_user_error'
46
+
11
47
  require 'graphql_devise/concerns/controller_methods'
12
48
  require 'graphql_devise/schema'
13
49
  require 'graphql_devise/types/authenticatable_type'
@@ -18,13 +54,10 @@ require 'graphql_devise/default_operations/mutations'
18
54
  require 'graphql_devise/default_operations/resolvers'
19
55
  require 'graphql_devise/resolvers/dummy'
20
56
 
21
- require 'graphql_devise/engine'
22
- require 'graphql_devise/version'
23
- require 'graphql_devise/error_codes'
24
- require 'graphql_devise/user_error'
25
- require 'graphql_devise/detailed_user_error'
26
-
27
57
  require 'graphql_devise/mount_method/option_sanitizer'
28
58
  require 'graphql_devise/mount_method/options_validator'
29
59
  require 'graphql_devise/mount_method/operation_preparer'
30
60
  require 'graphql_devise/mount_method/operation_sanitizer'
61
+
62
+ require 'graphql_devise/resource_loader'
63
+ require 'graphql_devise/schema_plugin'
@@ -9,12 +9,12 @@ require 'graphql_devise/mutations/update_password'
9
9
  module GraphqlDevise
10
10
  module DefaultOperations
11
11
  MUTATIONS = {
12
- login: GraphqlDevise::Mutations::Login,
13
- logout: GraphqlDevise::Mutations::Logout,
14
- sign_up: GraphqlDevise::Mutations::SignUp,
15
- update_password: GraphqlDevise::Mutations::UpdatePassword,
16
- send_password_reset: GraphqlDevise::Mutations::SendPasswordReset,
17
- resend_confirmation: GraphqlDevise::Mutations::ResendConfirmation
12
+ login: { klass: GraphqlDevise::Mutations::Login, authenticatable: true },
13
+ logout: { klass: GraphqlDevise::Mutations::Logout, authenticatable: true },
14
+ sign_up: { klass: GraphqlDevise::Mutations::SignUp, authenticatable: true },
15
+ update_password: { klass: GraphqlDevise::Mutations::UpdatePassword, authenticatable: true },
16
+ send_password_reset: { klass: GraphqlDevise::Mutations::SendPasswordReset, authenticatable: false },
17
+ resend_confirmation: { klass: GraphqlDevise::Mutations::ResendConfirmation, authenticatable: false }
18
18
  }.freeze
19
19
  end
20
20
  end
@@ -5,8 +5,8 @@ require 'graphql_devise/resolvers/confirm_account'
5
5
  module GraphqlDevise
6
6
  module DefaultOperations
7
7
  QUERIES = {
8
- confirm_account: GraphqlDevise::Resolvers::ConfirmAccount,
9
- check_password_token: GraphqlDevise::Resolvers::CheckPasswordToken
8
+ confirm_account: { klass: GraphqlDevise::Resolvers::ConfirmAccount },
9
+ check_password_token: { klass: GraphqlDevise::Resolvers::CheckPasswordToken }
10
10
  }.freeze
11
11
  end
12
12
  end
@@ -0,0 +1,7 @@
1
+ module GraphqlDevise
2
+ class AuthenticationError < ExecutionError
3
+ def to_h
4
+ super.merge(extensions: { code: ERROR_CODES.fetch(:authentication_error) })
5
+ end
6
+ end
7
+ end
@@ -1,5 +1,5 @@
1
1
  module GraphqlDevise
2
- class DetailedUserError < GraphQL::ExecutionError
2
+ class DetailedUserError < ExecutionError
3
3
  def initialize(message, errors:)
4
4
  @message = message
5
5
  @errors = errors
@@ -0,0 +1,6 @@
1
+ module GraphqlDevise
2
+ ERROR_CODES = {
3
+ user_error: 'USER_ERROR',
4
+ authentication_error: 'AUTHENTICATION_ERROR'
5
+ }.freeze
6
+ end
@@ -0,0 +1,4 @@
1
+ module GraphqlDevise
2
+ class ExecutionError < GraphQL::ExecutionError
3
+ end
4
+ end
@@ -1,5 +1,5 @@
1
1
  module GraphqlDevise
2
- class UserError < GraphQL::ExecutionError
2
+ class UserError < ExecutionError
3
3
  def to_h
4
4
  super.merge(extensions: { code: ERROR_CODES.fetch(:user_error) })
5
5
  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
@@ -10,14 +10,18 @@ module GraphqlDevise
10
10
  end
11
11
 
12
12
  def call
13
- @selected_operations.except(*@custom_keys).each_with_object({}) do |(action, operation), result|
13
+ @selected_operations.except(*@custom_keys).each_with_object({}) do |(action, operation_info), result|
14
14
  mapped_action = "#{@mapping_name}_#{action}"
15
+ operation = operation_info[:klass]
16
+ options = operation_info.except(:klass)
15
17
 
16
18
  result[mapped_action.to_sym] = [
17
19
  OperationPreparers::GqlNameSetter.new(mapped_action),
18
20
  @preparer,
19
21
  OperationPreparers::ResourceNameSetter.new(@mapping_name)
20
- ].reduce(child_class(operation)) { |prepared_operation, preparer| preparer.call(prepared_operation) }
22
+ ].reduce(child_class(operation)) do |prepared_operation, preparer|
23
+ preparer.call(prepared_operation, **options)
24
+ end
21
25
  end
22
26
  end
23
27
 
@@ -6,7 +6,7 @@ module GraphqlDevise
6
6
  @mapping_name = mapping_name
7
7
  end
8
8
 
9
- def call(operation)
9
+ def call(operation, **)
10
10
  operation.graphql_name(graphql_name)
11
11
 
12
12
  operation
@@ -6,9 +6,10 @@ module GraphqlDevise
6
6
  @authenticatable_type = authenticatable_type
7
7
  end
8
8
 
9
- def call(mutation)
10
- mutation.field(:authenticatable, @authenticatable_type, null: false)
9
+ def call(mutation, authenticatable: true)
10
+ return mutation unless authenticatable
11
11
 
12
+ mutation.field(:authenticatable, @authenticatable_type, null: false)
12
13
  mutation
13
14
  end
14
15
  end
@@ -6,7 +6,7 @@ module GraphqlDevise
6
6
  @authenticatable_type = authenticatable_type
7
7
  end
8
8
 
9
- def call(resolver)
9
+ def call(resolver, **)
10
10
  resolver.type(@authenticatable_type, null: false)
11
11
 
12
12
  resolver