graphql_devise 0.14.1 → 0.17.0

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