graphql_devise 0.14.1 → 0.17.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +118 -0
  3. data/Appraisals +26 -6
  4. data/CHANGELOG.md +72 -6
  5. data/README.md +184 -69
  6. data/Rakefile +2 -1
  7. data/app/controllers/graphql_devise/concerns/additional_controller_methods.rb +72 -0
  8. data/app/controllers/graphql_devise/concerns/set_user_by_token.rb +5 -27
  9. data/app/controllers/graphql_devise/graphql_controller.rb +1 -1
  10. data/app/helpers/graphql_devise/mailer_helper.rb +2 -2
  11. data/app/models/graphql_devise/concerns/additional_model_methods.rb +21 -0
  12. data/app/models/graphql_devise/concerns/model.rb +6 -9
  13. data/app/views/graphql_devise/mailer/confirmation_instructions.html.erb +7 -1
  14. data/graphql_devise.gemspec +1 -1
  15. data/lib/generators/graphql_devise/install_generator.rb +1 -1
  16. data/lib/graphql_devise.rb +20 -6
  17. data/lib/graphql_devise/concerns/controller_methods.rb +3 -3
  18. data/lib/graphql_devise/default_operations/mutations.rb +14 -8
  19. data/lib/graphql_devise/default_operations/resolvers.rb +2 -2
  20. data/lib/graphql_devise/model/with_email_updater.rb +34 -8
  21. data/lib/graphql_devise/mount_method/operation_preparer.rb +6 -6
  22. data/lib/graphql_devise/mount_method/operation_preparers/custom_operation_preparer.rb +6 -4
  23. data/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb +7 -5
  24. data/lib/graphql_devise/mount_method/operation_preparers/{resource_name_setter.rb → resource_klass_setter.rb} +4 -4
  25. data/lib/graphql_devise/mount_method/operation_sanitizer.rb +13 -1
  26. data/lib/graphql_devise/mutations/confirm_registration_with_token.rb +30 -0
  27. data/lib/graphql_devise/mutations/register.rb +60 -0
  28. data/lib/graphql_devise/mutations/resend_confirmation_with_token.rb +44 -0
  29. data/lib/graphql_devise/mutations/sign_up.rb +1 -1
  30. data/lib/graphql_devise/resolvers/confirm_account.rb +1 -1
  31. data/lib/graphql_devise/resource_loader.rb +26 -11
  32. data/lib/graphql_devise/schema_plugin.rb +31 -10
  33. data/lib/graphql_devise/version.rb +1 -1
  34. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +13 -2
  35. data/spec/dummy/app/graphql/dummy_schema.rb +8 -6
  36. data/spec/dummy/app/graphql/mutations/register.rb +14 -0
  37. data/spec/dummy/app/graphql/types/query_type.rb +5 -0
  38. data/spec/dummy/config/routes.rb +7 -5
  39. data/spec/dummy/db/migrate/20200623003142_create_schema_users.rb +0 -1
  40. data/spec/dummy/db/migrate/20210516211417_add_vip_to_users.rb +5 -0
  41. data/spec/dummy/db/schema.rb +4 -4
  42. data/spec/generators/graphql_devise/install_generator_spec.rb +1 -1
  43. data/spec/graphql/user_queries_spec.rb +3 -1
  44. data/spec/graphql_devise/model/with_email_updater_spec.rb +97 -68
  45. data/spec/requests/graphql_controller_spec.rb +12 -11
  46. data/spec/requests/mutations/confirm_registration_with_token_spec.rb +117 -0
  47. data/spec/requests/mutations/register_spec.rb +166 -0
  48. data/spec/requests/mutations/resend_confirmation_with_token_spec.rb +137 -0
  49. data/spec/requests/queries/introspection_query_spec.rb +149 -0
  50. data/spec/requests/user_controller_spec.rb +86 -25
  51. data/spec/services/mount_method/operation_preparer_spec.rb +5 -5
  52. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +5 -5
  53. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +5 -5
  54. data/spec/services/mount_method/operation_preparers/{resource_name_setter_spec.rb → resource_klass_setter_spec.rb} +6 -6
  55. data/spec/services/mount_method/operation_sanitizer_spec.rb +3 -3
  56. data/spec/services/resource_loader_spec.rb +5 -5
  57. data/spec/support/contexts/graphql_request.rb +11 -3
  58. metadata +29 -12
  59. data/.travis.yml +0 -86
@@ -4,19 +4,21 @@ module GraphqlDevise
4
4
  module MountMethod
5
5
  module OperationPreparers
6
6
  class CustomOperationPreparer
7
- def initialize(selected_keys:, custom_operations:, mapping_name:)
7
+ def initialize(selected_keys:, custom_operations:, model:)
8
8
  @selected_keys = selected_keys
9
9
  @custom_operations = custom_operations
10
- @mapping_name = mapping_name
10
+ @model = model
11
11
  end
12
12
 
13
13
  def call
14
+ mapping_name = GraphqlDevise.to_mapping_name(@model)
15
+
14
16
  @custom_operations.slice(*@selected_keys).each_with_object({}) do |(action, operation), result|
15
- mapped_action = "#{@mapping_name}_#{action}"
17
+ mapped_action = "#{mapping_name}_#{action}"
16
18
 
17
19
  result[mapped_action.to_sym] = [
18
20
  OperationPreparers::GqlNameSetter.new(mapped_action),
19
- OperationPreparers::ResourceNameSetter.new(@mapping_name)
21
+ OperationPreparers::ResourceKlassSetter.new(@model)
20
22
  ].reduce(operation) { |prepared_operation, preparer| preparer.call(prepared_operation) }
21
23
  end
22
24
  end
@@ -4,23 +4,25 @@ module GraphqlDevise
4
4
  module MountMethod
5
5
  module OperationPreparers
6
6
  class DefaultOperationPreparer
7
- def initialize(selected_operations:, custom_keys:, mapping_name:, preparer:)
7
+ def initialize(selected_operations:, custom_keys:, model:, preparer:)
8
8
  @selected_operations = selected_operations
9
9
  @custom_keys = custom_keys
10
- @mapping_name = mapping_name
10
+ @model = model
11
11
  @preparer = preparer
12
12
  end
13
13
 
14
14
  def call
15
+ mapping_name = GraphqlDevise.to_mapping_name(@model)
16
+
15
17
  @selected_operations.except(*@custom_keys).each_with_object({}) do |(action, operation_info), result|
16
- mapped_action = "#{@mapping_name}_#{action}"
18
+ mapped_action = "#{mapping_name}_#{action}"
17
19
  operation = operation_info[:klass]
18
- options = operation_info.except(:klass)
20
+ options = operation_info.except(:klass, :deprecation_reason)
19
21
 
20
22
  result[mapped_action.to_sym] = [
21
23
  OperationPreparers::GqlNameSetter.new(mapped_action),
22
24
  @preparer,
23
- OperationPreparers::ResourceNameSetter.new(@mapping_name)
25
+ OperationPreparers::ResourceKlassSetter.new(@model)
24
26
  ].reduce(child_class(operation)) do |prepared_operation, preparer|
25
27
  preparer.call(prepared_operation, **options)
26
28
  end
@@ -3,13 +3,13 @@
3
3
  module GraphqlDevise
4
4
  module MountMethod
5
5
  module OperationPreparers
6
- class ResourceNameSetter
7
- def initialize(name)
8
- @name = name
6
+ class ResourceKlassSetter
7
+ def initialize(klass)
8
+ @klass = klass
9
9
  end
10
10
 
11
11
  def call(operation, **)
12
- operation.instance_variable_set(:@resource_name, @name)
12
+ operation.instance_variable_set(:@resource_klass, @klass)
13
13
 
14
14
  operation
15
15
  end
@@ -18,13 +18,25 @@ module GraphqlDevise
18
18
  end
19
19
 
20
20
  def call
21
- if @only.present?
21
+ operations = if @only.present?
22
22
  @default.slice(*@only)
23
23
  elsif @skipped.present?
24
24
  @default.except(*@skipped)
25
25
  else
26
26
  @default
27
27
  end
28
+
29
+ operations.each do |operation, values|
30
+ next if values[:deprecation_reason].blank?
31
+
32
+ ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
33
+ `#{operation}` is deprecated and will be removed in a future version of this gem.
34
+ #{values[:deprecation_reason]}
35
+
36
+ You can supress this message by skipping `#{operation}` on your ResourceLoader or the
37
+ mount_graphql_devise_for method on your routes file.
38
+ DEPRECATION
39
+ end
28
40
  end
29
41
  end
30
42
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlDevise
4
+ module Mutations
5
+ class ConfirmRegistrationWithToken < Base
6
+ argument :confirmation_token, String, required: true
7
+
8
+ field :credentials,
9
+ GraphqlDevise::Types::CredentialType,
10
+ null: true,
11
+ description: 'Authentication credentials. Null unless user is signed in after confirmation.'
12
+
13
+ def resolve(confirmation_token:)
14
+ resource = resource_class.confirm_by_token(confirmation_token)
15
+
16
+ if resource.errors.empty?
17
+ yield resource if block_given?
18
+
19
+ response_payload = { authenticatable: resource }
20
+
21
+ response_payload[:credentials] = set_auth_headers(resource) if resource.active_for_authentication?
22
+
23
+ response_payload
24
+ else
25
+ raise_user_error(I18n.t('graphql_devise.confirmations.invalid_token'))
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlDevise
4
+ module Mutations
5
+ class Register < Base
6
+ argument :email, String, required: true
7
+ argument :password, String, required: true
8
+ argument :password_confirmation, String, required: true
9
+ argument :confirm_url, String, required: false
10
+
11
+ field :credentials,
12
+ GraphqlDevise::Types::CredentialType,
13
+ null: true,
14
+ description: 'Authentication credentials. Null if after signUp resource is not active for authentication (e.g. Email confirmation required).'
15
+
16
+ def resolve(confirm_url: nil, **attrs)
17
+ resource = build_resource(attrs.merge(provider: provider))
18
+ raise_user_error(I18n.t('graphql_devise.resource_build_failed')) if resource.blank?
19
+
20
+ redirect_url = confirm_url || DeviseTokenAuth.default_confirm_success_url
21
+ if confirmable_enabled? && redirect_url.blank?
22
+ raise_user_error(I18n.t('graphql_devise.registrations.missing_confirm_redirect_url'))
23
+ end
24
+
25
+ check_redirect_url_whitelist!(redirect_url)
26
+
27
+ resource.skip_confirmation_notification! if resource.respond_to?(:skip_confirmation_notification!)
28
+
29
+ if resource.save
30
+ yield resource if block_given?
31
+
32
+ unless resource.confirmed?
33
+ resource.send_confirmation_instructions(
34
+ redirect_url: redirect_url,
35
+ template_path: ['graphql_devise/mailer']
36
+ )
37
+ end
38
+
39
+ response_payload = { authenticatable: resource }
40
+
41
+ response_payload[:credentials] = set_auth_headers(resource) if resource.active_for_authentication?
42
+
43
+ response_payload
44
+ else
45
+ resource.try(:clean_up_passwords)
46
+ raise_user_error_list(
47
+ I18n.t('graphql_devise.registration_failed'),
48
+ errors: resource.errors.full_messages
49
+ )
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def build_resource(attrs)
56
+ resource_class.new(attrs)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlDevise
4
+ module Mutations
5
+ class ResendConfirmationWithToken < Base
6
+ argument :email, String, required: true
7
+ argument :confirm_url, String, required: true
8
+
9
+ field :message, String, null: false
10
+
11
+ def resolve(email:, confirm_url:)
12
+ check_redirect_url_whitelist!(confirm_url)
13
+
14
+ resource = find_confirmable_resource(email)
15
+
16
+ if resource
17
+ yield resource if block_given?
18
+
19
+ if resource.confirmed? && !resource.pending_reconfirmation?
20
+ raise_user_error(I18n.t('graphql_devise.confirmations.already_confirmed'))
21
+ end
22
+
23
+ resource.send_confirmation_instructions(
24
+ redirect_url: confirm_url,
25
+ template_path: ['graphql_devise/mailer']
26
+ )
27
+
28
+ { message: I18n.t('graphql_devise.confirmations.send_instructions', email: email) }
29
+ else
30
+ raise_user_error(I18n.t('graphql_devise.confirmations.user_not_found', email: email))
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def find_confirmable_resource(email)
37
+ email_insensitive = get_case_insensitive_field(:email, email)
38
+ resource = find_resource(:unconfirmed_email, email_insensitive) if resource_class.reconfirmable
39
+ resource ||= find_resource(:email, email_insensitive)
40
+ resource
41
+ end
42
+ end
43
+ end
44
+ end
@@ -31,7 +31,7 @@ module GraphqlDevise
31
31
 
32
32
  unless resource.confirmed?
33
33
  resource.send_confirmation_instructions(
34
- redirect_url: confirm_success_url,
34
+ redirect_url: redirect_url,
35
35
  template_path: ['graphql_devise/mailer'],
36
36
  schema_url: controller.full_url_without_params
37
37
  )
@@ -32,7 +32,7 @@ module GraphqlDevise
32
32
  end
33
33
 
34
34
  controller.redirect_to(redirect_to_link)
35
- { authenticatable: resource }
35
+ resource
36
36
  else
37
37
  raise_user_error(I18n.t('graphql_devise.confirmations.invalid_token'))
38
38
  end
@@ -10,12 +10,27 @@ module GraphqlDevise
10
10
  end
11
11
 
12
12
  def call(query, mutation)
13
- mapping_name = @resource.to_s.underscore.tr('/', '_').to_sym
14
-
15
13
  # clean_options responds to all keys defined in GraphqlDevise::MountMethod::SUPPORTED_OPTIONS
16
14
  clean_options = GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call!
17
15
 
18
- return clean_options if GraphqlDevise.resource_mounted?(mapping_name) && @routing
16
+ model = if @resource.is_a?(String)
17
+ ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
18
+ Providing a String as the model you want to mount is deprecated and will be removed in a future version of
19
+ this gem. Please use the actual model constant instead.
20
+
21
+ EXAMPLE
22
+
23
+ GraphqlDevise::ResourceLoader.new(User) # instead of GraphqlDevise::ResourceLoader.new('User')
24
+
25
+ mount_graphql_devise_for User # instead of mount_graphql_devise_for 'User'
26
+ DEPRECATION
27
+ @resource.constantize
28
+ else
29
+ @resource
30
+ end
31
+
32
+ # Necesary when mounting a resource via route file as Devise forces the reloading of routes
33
+ return clean_options if GraphqlDevise.resource_mounted?(model) && @routing
19
34
 
20
35
  validate_options!(clean_options)
21
36
 
@@ -23,7 +38,7 @@ module GraphqlDevise
23
38
  "Types::#{@resource}Type".safe_constantize ||
24
39
  GraphqlDevise::Types::AuthenticatableType
25
40
 
26
- prepared_mutations = prepare_mutations(mapping_name, clean_options, authenticatable_type)
41
+ prepared_mutations = prepare_mutations(model, clean_options, authenticatable_type)
27
42
 
28
43
  if prepared_mutations.any? && mutation.blank?
29
44
  raise GraphqlDevise::Error, 'You need to provide a mutation type unless all mutations are skipped'
@@ -33,7 +48,7 @@ module GraphqlDevise
33
48
  mutation.field(action, mutation: prepared_mutation, authenticate: false)
34
49
  end
35
50
 
36
- prepared_resolvers = prepare_resolvers(mapping_name, clean_options, authenticatable_type)
51
+ prepared_resolvers = prepare_resolvers(model, clean_options, authenticatable_type)
37
52
 
38
53
  if prepared_resolvers.any? && query.blank?
39
54
  raise GraphqlDevise::Error, 'You need to provide a query type unless all queries are skipped'
@@ -43,17 +58,17 @@ module GraphqlDevise
43
58
  query.field(action, resolver: resolver, authenticate: false)
44
59
  end
45
60
 
46
- GraphqlDevise.add_mapping(mapping_name, @resource)
47
- GraphqlDevise.mount_resource(mapping_name) if @routing
61
+ GraphqlDevise.add_mapping(GraphqlDevise.to_mapping_name(@resource).to_sym, @resource)
62
+ GraphqlDevise.mount_resource(model) if @routing
48
63
 
49
64
  clean_options
50
65
  end
51
66
 
52
67
  private
53
68
 
54
- def prepare_resolvers(mapping_name, clean_options, authenticatable_type)
69
+ def prepare_resolvers(model, clean_options, authenticatable_type)
55
70
  GraphqlDevise::MountMethod::OperationPreparer.new(
56
- mapping_name: mapping_name,
71
+ model: model,
57
72
  custom: clean_options.operations,
58
73
  additional_operations: clean_options.additional_queries,
59
74
  preparer: GraphqlDevise::MountMethod::OperationPreparers::ResolverTypeSetter.new(authenticatable_type),
@@ -63,9 +78,9 @@ module GraphqlDevise
63
78
  ).call
64
79
  end
65
80
 
66
- def prepare_mutations(mapping_name, clean_options, authenticatable_type)
81
+ def prepare_mutations(model, clean_options, authenticatable_type)
67
82
  GraphqlDevise::MountMethod::OperationPreparer.new(
68
- mapping_name: mapping_name,
83
+ model: model,
69
84
  custom: clean_options.operations,
70
85
  additional_operations: clean_options.additional_mutations,
71
86
  preparer: GraphqlDevise::MountMethod::OperationPreparers::MutationFieldSetter.new(authenticatable_type),
@@ -2,18 +2,20 @@
2
2
 
3
3
  module GraphqlDevise
4
4
  class SchemaPlugin
5
+ # NOTE: Based on GQL-Ruby docs https://graphql-ruby.org/schema/introspection.html
6
+ INTROSPECTION_FIELDS = ['__schema', '__type', '__typename']
5
7
  DEFAULT_NOT_AUTHENTICATED = ->(field) { raise GraphqlDevise::AuthenticationError, "#{field} field requires authentication" }
6
8
 
7
- def initialize(query: nil, mutation: nil, authenticate_default: true, resource_loaders: [], unauthenticated_proc: DEFAULT_NOT_AUTHENTICATED)
9
+ def initialize(query: nil, mutation: nil, authenticate_default: true, public_introspection: !Rails.env.production?, resource_loaders: [], unauthenticated_proc: DEFAULT_NOT_AUTHENTICATED)
8
10
  @query = query
9
11
  @mutation = mutation
10
12
  @resource_loaders = resource_loaders
11
13
  @authenticate_default = authenticate_default
14
+ @public_introspection = public_introspection
12
15
  @unauthenticated_proc = unauthenticated_proc
13
16
 
14
17
  # Must happen on initialize so operations are loaded before the types are added to the schema on GQL < 1.10
15
18
  load_fields
16
- reconfigure_warden!
17
19
  end
18
20
 
19
21
  def use(schema_definition)
@@ -28,9 +30,23 @@ module GraphqlDevise
28
30
  auth_required = authenticate_option(field, trace_data)
29
31
  context = context_from_data(trace_data)
30
32
 
31
- if auth_required
33
+ if context.key?(:resource_name)
34
+ ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
35
+ Providing `resource_name` as part of the GQL context, or doing so by using the `graphql_context(resource_name)`
36
+ method on your controller is deprecated and will be removed in a future version of this gem.
37
+ Please use `gql_devise_context` in you controller instead.
38
+
39
+ EXAMPLE
40
+ include GraphqlDevise::Concerns::SetUserByToken
41
+
42
+ DummySchema.execute(params[:query], context: gql_devise_context(User))
43
+ DummySchema.execute(params[:query], context: gql_devise_context(User, Admin))
44
+ DEPRECATION
45
+ end
46
+
47
+ if auth_required && !(public_introspection && introspection_field?(field))
32
48
  context = set_current_resource(context)
33
- raise_on_missing_resource(context, field)
49
+ raise_on_missing_resource(context, field, auth_required)
34
50
  end
35
51
 
36
52
  yield
@@ -38,6 +54,8 @@ module GraphqlDevise
38
54
 
39
55
  private
40
56
 
57
+ attr_reader :public_introspection
58
+
41
59
  def set_current_resource(context)
42
60
  controller = context[:controller]
43
61
  resource_names = Array(context[:resource_name])
@@ -57,8 +75,12 @@ module GraphqlDevise
57
75
  context
58
76
  end
59
77
 
60
- def raise_on_missing_resource(context, field)
78
+ def raise_on_missing_resource(context, field, auth_required)
61
79
  @unauthenticated_proc.call(field.name) if context[:current_resource].blank?
80
+
81
+ if auth_required.respond_to?(:call) && !auth_required.call(context[:current_resource])
82
+ @unauthenticated_proc.call(field.name)
83
+ end
62
84
  end
63
85
 
64
86
  def context_from_data(trace_data)
@@ -97,11 +119,6 @@ module GraphqlDevise
97
119
  auth_required.nil? ? @authenticate_default : auth_required
98
120
  end
99
121
 
100
- def reconfigure_warden!
101
- Devise.class_variable_set(:@@warden_configured, nil)
102
- Devise.configure_warden!
103
- end
104
-
105
122
  def load_fields
106
123
  @resource_loaders.each do |resource_loader|
107
124
  raise Error, 'Invalid resource loader instance' unless resource_loader.instance_of?(GraphqlDevise::ResourceLoader)
@@ -109,6 +126,10 @@ module GraphqlDevise
109
126
  resource_loader.call(@query, @mutation)
110
127
  end
111
128
  end
129
+
130
+ def introspection_field?(field)
131
+ INTROSPECTION_FIELDS.include?(field.name)
132
+ end
112
133
  end
113
134
  end
114
135
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlDevise
4
- VERSION = '0.14.1'.freeze
4
+ VERSION = '0.17.0'.freeze
5
5
  end