graphql_devise 0.11.2 → 0.12.2

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +9 -3
  5. data/CHANGELOG.md +50 -1
  6. data/README.md +185 -32
  7. data/app/controllers/graphql_devise/application_controller.rb +4 -1
  8. data/app/controllers/graphql_devise/concerns/set_user_by_token.rb +25 -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/app/views/graphql_devise/mailer/confirmation_instructions.html.erb +1 -1
  12. data/app/views/graphql_devise/mailer/reset_password_instructions.html.erb +1 -1
  13. data/config/locales/en.yml +1 -0
  14. data/config/routes.rb +2 -0
  15. data/graphql_devise.gemspec +6 -4
  16. data/lib/generators/graphql_devise/install_generator.rb +63 -30
  17. data/lib/graphql_devise.rb +24 -10
  18. data/lib/graphql_devise/default_operations/mutations.rb +6 -6
  19. data/lib/graphql_devise/default_operations/resolvers.rb +2 -2
  20. data/lib/graphql_devise/errors/authentication_error.rb +7 -0
  21. data/lib/graphql_devise/{detailed_user_error.rb → errors/detailed_user_error.rb} +1 -1
  22. data/lib/graphql_devise/errors/error_codes.rb +6 -0
  23. data/lib/graphql_devise/errors/execution_error.rb +4 -0
  24. data/lib/graphql_devise/{user_error.rb → errors/user_error.rb} +1 -1
  25. data/lib/graphql_devise/mount_method/operation_preparer.rb +2 -2
  26. data/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb +6 -2
  27. data/lib/graphql_devise/mount_method/operation_preparers/gql_name_setter.rb +1 -1
  28. data/lib/graphql_devise/mount_method/operation_preparers/mutation_field_setter.rb +3 -2
  29. data/lib/graphql_devise/mount_method/operation_preparers/resolver_type_setter.rb +1 -1
  30. data/lib/graphql_devise/mount_method/operation_preparers/resource_name_setter.rb +2 -2
  31. data/lib/graphql_devise/mutations/resend_confirmation.rb +3 -5
  32. data/lib/graphql_devise/mutations/send_password_reset.rb +5 -2
  33. data/lib/graphql_devise/mutations/sign_up.rb +3 -6
  34. data/lib/graphql_devise/rails/routes.rb +5 -72
  35. data/lib/graphql_devise/resource_loader.rb +87 -0
  36. data/lib/graphql_devise/schema_plugin.rb +106 -0
  37. data/lib/graphql_devise/version.rb +1 -1
  38. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +41 -3
  39. data/spec/dummy/app/controllers/application_controller.rb +1 -0
  40. data/spec/dummy/app/graphql/dummy_schema.rb +18 -0
  41. data/spec/dummy/app/graphql/interpreter_schema.rb +9 -0
  42. data/spec/dummy/app/graphql/types/mutation_type.rb +1 -1
  43. data/spec/dummy/app/graphql/types/query_type.rb +10 -0
  44. data/spec/dummy/config/routes.rb +3 -0
  45. data/spec/generators/graphql_devise/install_generator_spec.rb +62 -30
  46. data/spec/rails_helper.rb +4 -1
  47. data/spec/requests/graphql_controller_spec.rb +80 -0
  48. data/spec/requests/mutations/resend_confirmation_spec.rb +44 -29
  49. data/spec/requests/mutations/send_password_reset_spec.rb +40 -12
  50. data/spec/requests/queries/confirm_account_spec.rb +7 -1
  51. data/spec/requests/user_controller_spec.rb +189 -24
  52. data/spec/services/mount_method/operation_preparer_spec.rb +8 -3
  53. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +1 -1
  54. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +15 -8
  55. data/spec/services/mount_method/operation_preparers/mutation_field_setter_spec.rb +18 -4
  56. data/spec/services/mount_method/operation_preparers/resource_name_setter_spec.rb +1 -1
  57. data/spec/services/resource_loader_spec.rb +82 -0
  58. data/spec/services/schema_plugin_spec.rb +26 -0
  59. data/spec/spec_helper.rb +1 -1
  60. metadata +107 -89
  61. data/lib/graphql_devise/error_codes.rb +0 -5
  62. data/spec/support/generators/file_helpers.rb +0 -12
@@ -4,6 +4,8 @@ module GraphqlDevise
4
4
  argument :email, String, required: true
5
5
  argument :redirect_url, String, required: true
6
6
 
7
+ field :message, String, null: false
8
+
7
9
  def resolve(email:, redirect_url:)
8
10
  resource = find_resource(:email, get_case_insensitive_field(:email, email))
9
11
 
@@ -14,11 +16,12 @@ module GraphqlDevise
14
16
  email: email,
15
17
  provider: 'email',
16
18
  redirect_url: redirect_url,
17
- template_path: ['graphql_devise/mailer']
19
+ template_path: ['graphql_devise/mailer'],
20
+ **controller.params.permit('controller', 'action').to_h.symbolize_keys
18
21
  )
19
22
 
20
23
  if resource.errors.empty?
21
- { authenticatable: resource }
24
+ { message: I18n.t('graphql_devise.passwords.send_instructions') }
22
25
  else
23
26
  raise_user_error_list(I18n.t('graphql_devise.invalid_resource'), errors: resource.errors.full_messages)
24
27
  end
@@ -27,7 +27,8 @@ module GraphqlDevise
27
27
  unless resource.confirmed?
28
28
  resource.send_confirmation_instructions(
29
29
  redirect_url: confirm_success_url,
30
- template_path: ['graphql_devise/mailer']
30
+ template_path: ['graphql_devise/mailer'],
31
+ **controller.params.permit('controller', 'action').to_h.symbolize_keys
31
32
  )
32
33
  end
33
34
 
@@ -35,7 +36,7 @@ module GraphqlDevise
35
36
 
36
37
  { authenticatable: resource }
37
38
  else
38
- clean_up_passwords(resource)
39
+ resource.try(:clean_up_passwords)
39
40
  raise_user_error_list(
40
41
  I18n.t('graphql_devise.registration_failed'),
41
42
  errors: resource.errors.full_messages
@@ -48,10 +49,6 @@ module GraphqlDevise
48
49
  def build_resource(attrs)
49
50
  resource_class.new(attrs)
50
51
  end
51
-
52
- def clean_up_passwords(resource)
53
- controller.send(:clean_up_passwords, resource)
54
- end
55
52
  end
56
53
  end
57
54
  end
@@ -1,80 +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
- devise_for(
29
- resource.pluralize.underscore.tr('/', '_').to_sym,
30
- module: :devise,
31
- class_name: resource,
32
- skip: DEVISE_OPERATIONS
4
+ clean_options = GraphqlDevise::ResourceLoader.new(resource, options, true).call(
5
+ GraphqlDevise::Types::QueryType,
6
+ GraphqlDevise::Types::MutationType
33
7
  )
34
8
 
35
- devise_scope resource.underscore.tr('/', '_').to_sym do
36
- post clean_options.at, to: 'graphql_devise/graphql#auth'
37
- get clean_options.at, to: 'graphql_devise/graphql#auth'
38
- end
39
-
40
- # Avoid routes reload done by Devise
41
- return if GraphqlDevise.resource_mounted?(resource)
42
-
43
- authenticatable_type = clean_options.authenticatable_type.presence ||
44
- "Types::#{resource}Type".safe_constantize ||
45
- GraphqlDevise::Types::AuthenticatableType
46
-
47
- prepared_mutations = GraphqlDevise::MountMethod::OperationPreparer.new(
48
- resource: resource,
49
- custom: clean_options.operations,
50
- additional_operations: clean_options.additional_mutations,
51
- preparer: GraphqlDevise::MountMethod::OperationPreparers::MutationFieldSetter.new(authenticatable_type),
52
- selected_operations: GraphqlDevise::MountMethod::OperationSanitizer.call(
53
- default: GraphqlDevise::DefaultOperations::MUTATIONS, only: clean_options.only, skipped: clean_options.skip
54
- )
55
- ).call
56
-
57
- prepared_mutations.each do |action, mutation|
58
- GraphqlDevise::Types::MutationType.field(action, mutation: mutation)
59
- end
60
-
61
- prepared_queries = GraphqlDevise::MountMethod::OperationPreparer.new(
62
- resource: resource,
63
- custom: clean_options.operations,
64
- additional_operations: clean_options.additional_queries,
65
- preparer: GraphqlDevise::MountMethod::OperationPreparers::ResolverTypeSetter.new(authenticatable_type),
66
- selected_operations: GraphqlDevise::MountMethod::OperationSanitizer.call(
67
- default: GraphqlDevise::DefaultOperations::QUERIES, only: clean_options.only, skipped: clean_options.skip
68
- )
69
- ).call
70
-
71
- prepared_queries.each do |action, resolver|
72
- GraphqlDevise::Types::QueryType.field(action, resolver: resolver)
73
- end
74
-
75
- Devise.mailer.helper(GraphqlDevise::MailerHelper)
76
-
77
- GraphqlDevise.mount_resource(resource)
9
+ post clean_options.at, to: 'graphql_devise/graphql#auth'
10
+ get clean_options.at, to: 'graphql_devise/graphql#auth'
78
11
  end
79
12
  end
80
13
  end
@@ -0,0 +1,87 @@
1
+ module GraphqlDevise
2
+ class ResourceLoader
3
+ def initialize(resource, options = {}, routing = false)
4
+ @resource = resource
5
+ @options = options
6
+ @routing = routing
7
+ @default_operations = GraphqlDevise::DefaultOperations::MUTATIONS.merge(GraphqlDevise::DefaultOperations::QUERIES)
8
+ end
9
+
10
+ def call(query, mutation)
11
+ mapping_name = @resource.to_s.underscore.tr('/', '_').to_sym
12
+
13
+ # clean_options responds to all keys defined in GraphqlDevise::MountMethod::SUPPORTED_OPTIONS
14
+ clean_options = GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call!
15
+
16
+ return clean_options if GraphqlDevise.resource_mounted?(mapping_name) && @routing
17
+
18
+ validate_options!(clean_options)
19
+
20
+ authenticatable_type = clean_options.authenticatable_type.presence ||
21
+ "Types::#{@resource}Type".safe_constantize ||
22
+ GraphqlDevise::Types::AuthenticatableType
23
+
24
+ prepared_mutations = prepare_mutations(mapping_name, clean_options, authenticatable_type)
25
+
26
+ if prepared_mutations.any? && mutation.blank?
27
+ raise GraphqlDevise::Error, 'You need to provide a mutation type unless all mutations are skipped'
28
+ end
29
+
30
+ prepared_mutations.each do |action, prepared_mutation|
31
+ mutation.field(action, mutation: prepared_mutation, authenticate: false)
32
+ end
33
+
34
+ prepared_resolvers = prepare_resolvers(mapping_name, clean_options, authenticatable_type)
35
+
36
+ if prepared_resolvers.any? && query.blank?
37
+ raise GraphqlDevise::Error, 'You need to provide a query type unless all queries are skipped'
38
+ end
39
+
40
+ prepared_resolvers.each do |action, resolver|
41
+ query.field(action, resolver: resolver, authenticate: false)
42
+ end
43
+
44
+ GraphqlDevise.add_mapping(mapping_name, @resource)
45
+ GraphqlDevise.mount_resource(mapping_name) if @routing
46
+
47
+ clean_options
48
+ end
49
+
50
+ private
51
+
52
+ def prepare_resolvers(mapping_name, clean_options, authenticatable_type)
53
+ GraphqlDevise::MountMethod::OperationPreparer.new(
54
+ mapping_name: mapping_name,
55
+ custom: clean_options.operations,
56
+ additional_operations: clean_options.additional_queries,
57
+ preparer: GraphqlDevise::MountMethod::OperationPreparers::ResolverTypeSetter.new(authenticatable_type),
58
+ selected_operations: GraphqlDevise::MountMethod::OperationSanitizer.call(
59
+ default: GraphqlDevise::DefaultOperations::QUERIES, only: clean_options.only, skipped: clean_options.skip
60
+ )
61
+ ).call
62
+ end
63
+
64
+ def prepare_mutations(mapping_name, clean_options, authenticatable_type)
65
+ GraphqlDevise::MountMethod::OperationPreparer.new(
66
+ mapping_name: mapping_name,
67
+ custom: clean_options.operations,
68
+ additional_operations: clean_options.additional_mutations,
69
+ preparer: GraphqlDevise::MountMethod::OperationPreparers::MutationFieldSetter.new(authenticatable_type),
70
+ selected_operations: GraphqlDevise::MountMethod::OperationSanitizer.call(
71
+ default: GraphqlDevise::DefaultOperations::MUTATIONS, only: clean_options.only, skipped: clean_options.skip
72
+ )
73
+ ).call
74
+ end
75
+
76
+ def validate_options!(clean_options)
77
+ GraphqlDevise::MountMethod::OptionsValidator.new(
78
+ [
79
+ GraphqlDevise::MountMethod::OptionValidators::SkipOnlyValidator.new(options: clean_options),
80
+ GraphqlDevise::MountMethod::OptionValidators::ProvidedOperationsValidator.new(
81
+ options: clean_options, supported_operations: @default_operations
82
+ )
83
+ ]
84
+ ).validate!
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,106 @@
1
+ module GraphqlDevise
2
+ class SchemaPlugin
3
+ DEFAULT_NOT_AUTHENTICATED = ->(field) { raise GraphqlDevise::AuthenticationError, "#{field} field requires authentication" }
4
+
5
+ def initialize(query: nil, mutation: nil, authenticate_default: true, resource_loaders: [], unauthenticated_proc: DEFAULT_NOT_AUTHENTICATED)
6
+ @query = query
7
+ @mutation = mutation
8
+ @resource_loaders = resource_loaders
9
+ @authenticate_default = authenticate_default
10
+ @unauthenticated_proc = unauthenticated_proc
11
+
12
+ # Must happen on initialize so operations are loaded before the types are added to the schema on GQL < 1.10
13
+ load_fields
14
+ end
15
+
16
+ def use(schema_definition)
17
+ schema_definition.tracer(self)
18
+ end
19
+
20
+ def trace(event, trace_data)
21
+ # Authenticate only root level queries
22
+ return yield unless event == 'execute_field' && path(trace_data).count == 1
23
+
24
+ field = traced_field(trace_data)
25
+ provided_value = authenticate_option(field, trace_data)
26
+ context = set_current_resource(context_from_data(trace_data))
27
+
28
+ if !provided_value.nil?
29
+ raise_on_missing_resource(context, field) if provided_value
30
+ elsif @authenticate_default
31
+ raise_on_missing_resource(context, field)
32
+ end
33
+
34
+ yield
35
+ end
36
+
37
+ private
38
+
39
+ def set_current_resource(context)
40
+ controller = context[:controller]
41
+ resource_names = Array(context[:resource_name])
42
+ context[:current_resource] = resource_names.find do |resource_name|
43
+ unless Devise.mappings.key?(resource_name)
44
+ raise(
45
+ GraphqlDevise::Error,
46
+ "Invalid resource_name `#{resource_name}` provided to `graphql_context`. Possible values are: #{Devise.mappings.keys}."
47
+ )
48
+ end
49
+
50
+ found = controller.set_resource_by_token(resource_name)
51
+ break found if found
52
+ end
53
+
54
+ context
55
+ end
56
+
57
+ def raise_on_missing_resource(context, field)
58
+ @unauthenticated_proc.call(field.name) if context[:current_resource].blank?
59
+ end
60
+
61
+ def context_from_data(trace_data)
62
+ query = if trace_data[:context]
63
+ trace_data[:context].query
64
+ else
65
+ trace_data[:query]
66
+ end
67
+
68
+ query.context
69
+ end
70
+
71
+ def path(trace_data)
72
+ if trace_data[:context]
73
+ trace_data[:context].path
74
+ else
75
+ trace_data[:path]
76
+ end
77
+ end
78
+
79
+ def traced_field(trace_data)
80
+ if trace_data[:context]
81
+ trace_data[:context].field
82
+ else
83
+ trace_data[:field]
84
+ end
85
+ end
86
+
87
+ def authenticate_option(field, trace_data)
88
+ if trace_data[:context]
89
+ field.metadata[:authenticate]
90
+ else
91
+ field.graphql_definition.metadata[:authenticate]
92
+ end
93
+ end
94
+
95
+ def load_fields
96
+ @resource_loaders.each do |resource_loader|
97
+ raise Error, 'Invalid resource loader instance' unless resource_loader.instance_of?(GraphqlDevise::ResourceLoader)
98
+
99
+ resource_loader.call(@query, @mutation)
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ GraphQL::Field.accepts_definitions(authenticate: GraphQL::Define.assign_metadata_key(:authenticate))
106
+ GraphQL::Schema::Field.accepts_definition(:authenticate)
@@ -1,3 +1,3 @@
1
1
  module GraphqlDevise
2
- VERSION = '0.11.2'.freeze
2
+ VERSION = '0.12.2'.freeze
3
3
  end
@@ -3,10 +3,48 @@ module Api
3
3
  class GraphqlController < ApplicationController
4
4
  include GraphqlDevise::Concerns::SetUserByToken
5
5
 
6
- before_action :authenticate_user!
7
-
8
6
  def graphql
9
- render json: DummySchema.execute(params[:query])
7
+ result = DummySchema.execute(params[:query], execute_params(params))
8
+
9
+ render json: result unless performed?
10
+ end
11
+
12
+ def interpreter
13
+ render json: InterpreterSchema.execute(params[:query], execute_params(params))
14
+ end
15
+
16
+ def failing_resource_name
17
+ render json: DummySchema.execute(params[:query], context: graphql_context([:user, :fail]))
18
+ end
19
+
20
+ private
21
+
22
+ def execute_params(item)
23
+ {
24
+ operation_name: item[:operationName],
25
+ variables: ensure_hash(item[:variables]),
26
+ context: graphql_context(:user)
27
+ }
28
+ end
29
+
30
+ def ensure_hash(ambiguous_param)
31
+ case ambiguous_param
32
+ when String
33
+ if ambiguous_param.present?
34
+ ensure_hash(JSON.parse(ambiguous_param))
35
+ else
36
+ {}
37
+ end
38
+ when Hash, ActionController::Parameters
39
+ ambiguous_param
40
+ when nil
41
+ {}
42
+ else
43
+ raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
44
+ end
45
+ end
46
+
47
+ def verify_authenticity_token
10
48
  end
11
49
  end
12
50
  end
@@ -1,2 +1,3 @@
1
1
  class ApplicationController < ActionController::Base
2
+ protect_from_forgery with: :exception
2
3
  end
@@ -1,4 +1,22 @@
1
1
  class DummySchema < GraphQL::Schema
2
+ use GraphqlDevise::SchemaPlugin.new(
3
+ query: Types::QueryType,
4
+ mutation: Types::MutationType,
5
+ resource_loaders: [
6
+ GraphqlDevise::ResourceLoader.new(
7
+ 'User',
8
+ only: [
9
+ :login,
10
+ :confirm_account,
11
+ :send_password_reset,
12
+ :resend_confirmation,
13
+ :check_password_token
14
+ ]
15
+ ),
16
+ GraphqlDevise::ResourceLoader.new('Guest', only: [:logout])
17
+ ]
18
+ )
19
+
2
20
  mutation(Types::MutationType)
3
21
  query(Types::QueryType)
4
22
  end
@@ -0,0 +1,9 @@
1
+ class InterpreterSchema < GraphQL::Schema
2
+ use GraphQL::Execution::Interpreter if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.9.0')
3
+ use GraphQL::Analysis::AST if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.10.0')
4
+
5
+ use GraphqlDevise::SchemaPlugin.new(query: Types::QueryType, authenticate_default: false)
6
+
7
+ mutation(Types::MutationType)
8
+ query(Types::QueryType)
9
+ end
@@ -1,6 +1,6 @@
1
1
  module Types
2
2
  class MutationType < Types::BaseObject
3
- field :dummy_mutation, String, null: false
3
+ field :dummy_mutation, String, null: false, authenticate: true
4
4
 
5
5
  def dummy_mutation
6
6
  'Necessary so GraphQL gem does not complain about empty mutation type'
@@ -1,5 +1,15 @@
1
1
  module Types
2
2
  class QueryType < Types::BaseObject
3
3
  field :user, resolver: Resolvers::UserShow
4
+ field :public_field, String, null: false, authenticate: false
5
+ field :private_field, String, null: false, authenticate: true
6
+
7
+ def public_field
8
+ 'Field does not require authentication'
9
+ end
10
+
11
+ def private_field
12
+ 'Field will always require authentication'
13
+ end
4
14
  end
5
15
  end