graphql_devise 0.14.3 → 0.17.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +7 -0
  3. data/CHANGELOG.md +58 -0
  4. data/README.md +186 -82
  5. data/app/controllers/graphql_devise/concerns/additional_controller_methods.rb +72 -0
  6. data/app/controllers/graphql_devise/concerns/set_user_by_token.rb +5 -27
  7. data/app/helpers/graphql_devise/mailer_helper.rb +2 -2
  8. data/app/models/graphql_devise/concerns/additional_model_methods.rb +21 -0
  9. data/app/models/graphql_devise/concerns/model.rb +6 -9
  10. data/app/views/graphql_devise/mailer/confirmation_instructions.html.erb +7 -1
  11. data/lib/generators/graphql_devise/install_generator.rb +1 -1
  12. data/lib/graphql_devise.rb +20 -6
  13. data/lib/graphql_devise/concerns/controller_methods.rb +3 -3
  14. data/lib/graphql_devise/default_operations/mutations.rb +14 -8
  15. data/lib/graphql_devise/default_operations/resolvers.rb +2 -2
  16. data/lib/graphql_devise/model/with_email_updater.rb +34 -8
  17. data/lib/graphql_devise/mount_method/operation_preparer.rb +6 -6
  18. data/lib/graphql_devise/mount_method/operation_preparers/custom_operation_preparer.rb +6 -4
  19. data/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb +7 -5
  20. data/lib/graphql_devise/mount_method/operation_preparers/{resource_name_setter.rb → resource_klass_setter.rb} +4 -4
  21. data/lib/graphql_devise/mount_method/operation_sanitizer.rb +13 -1
  22. data/lib/graphql_devise/mutations/confirm_registration_with_token.rb +30 -0
  23. data/lib/graphql_devise/mutations/login.rb +2 -0
  24. data/lib/graphql_devise/mutations/register.rb +60 -0
  25. data/lib/graphql_devise/mutations/resend_confirmation_with_token.rb +44 -0
  26. data/lib/graphql_devise/mutations/sign_up.rb +1 -1
  27. data/lib/graphql_devise/resolvers/confirm_account.rb +1 -1
  28. data/lib/graphql_devise/resource_loader.rb +26 -11
  29. data/lib/graphql_devise/schema_plugin.rb +20 -8
  30. data/lib/graphql_devise/version.rb +1 -1
  31. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +11 -0
  32. data/spec/dummy/app/graphql/dummy_schema.rb +4 -3
  33. data/spec/dummy/app/graphql/mutations/register.rb +14 -0
  34. data/spec/dummy/app/graphql/types/query_type.rb +5 -0
  35. data/spec/dummy/config/routes.rb +7 -5
  36. data/spec/dummy/db/migrate/20210516211417_add_vip_to_users.rb +5 -0
  37. data/spec/dummy/db/schema.rb +4 -3
  38. data/spec/generators/graphql_devise/install_generator_spec.rb +1 -1
  39. data/spec/graphql/user_queries_spec.rb +3 -1
  40. data/spec/graphql_devise/model/with_email_updater_spec.rb +97 -68
  41. data/spec/requests/graphql_controller_spec.rb +1 -1
  42. data/spec/requests/mutations/confirm_registration_with_token_spec.rb +117 -0
  43. data/spec/requests/mutations/register_spec.rb +166 -0
  44. data/spec/requests/mutations/resend_confirmation_with_token_spec.rb +137 -0
  45. data/spec/requests/user_controller_spec.rb +86 -25
  46. data/spec/services/mount_method/operation_preparer_spec.rb +5 -5
  47. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +5 -5
  48. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +5 -5
  49. data/spec/services/mount_method/operation_preparers/{resource_name_setter_spec.rb → resource_klass_setter_spec.rb} +6 -6
  50. data/spec/services/mount_method/operation_sanitizer_spec.rb +3 -3
  51. data/spec/services/resource_loader_spec.rb +5 -5
  52. data/spec/support/contexts/graphql_request.rb +2 -2
  53. metadata +21 -6
@@ -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
@@ -24,6 +24,8 @@ module GraphqlDevise
24
24
 
25
25
  yield resource if block_given?
26
26
 
27
+ context[:current_resource] = resource if context[:current_resource].nil?
28
+
27
29
  { authenticatable: resource, credentials: new_headers }
28
30
  elsif resource && !active_for_authentication?(resource)
29
31
  if locked?(resource)
@@ -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),
@@ -16,7 +16,6 @@ module GraphqlDevise
16
16
 
17
17
  # Must happen on initialize so operations are loaded before the types are added to the schema on GQL < 1.10
18
18
  load_fields
19
- reconfigure_warden!
20
19
  end
21
20
 
22
21
  def use(schema_definition)
@@ -31,9 +30,23 @@ module GraphqlDevise
31
30
  auth_required = authenticate_option(field, trace_data)
32
31
  context = context_from_data(trace_data)
33
32
 
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
+
34
47
  if auth_required && !(public_introspection && introspection_field?(field))
35
48
  context = set_current_resource(context)
36
- raise_on_missing_resource(context, field)
49
+ raise_on_missing_resource(context, field, auth_required)
37
50
  end
38
51
 
39
52
  yield
@@ -62,8 +75,12 @@ module GraphqlDevise
62
75
  context
63
76
  end
64
77
 
65
- def raise_on_missing_resource(context, field)
78
+ def raise_on_missing_resource(context, field, auth_required)
66
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
67
84
  end
68
85
 
69
86
  def context_from_data(trace_data)
@@ -102,11 +119,6 @@ module GraphqlDevise
102
119
  auth_required.nil? ? @authenticate_default : auth_required
103
120
  end
104
121
 
105
- def reconfigure_warden!
106
- Devise.class_variable_set(:@@warden_configured, nil)
107
- Devise.configure_warden!
108
- end
109
-
110
122
  def load_fields
111
123
  @resource_loaders.each do |resource_loader|
112
124
  raise Error, 'Invalid resource loader instance' unless resource_loader.instance_of?(GraphqlDevise::ResourceLoader)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlDevise
4
- VERSION = '0.14.3'.freeze
4
+ VERSION = '0.17.1'.freeze
5
5
  end
@@ -19,6 +19,17 @@ module Api
19
19
  render json: DummySchema.execute(params[:query], context: graphql_context([:user, :fail]))
20
20
  end
21
21
 
22
+ def controller_auth
23
+ result = DummySchema.execute(
24
+ params[:query],
25
+ operation_name: params[:operationName],
26
+ variables: ensure_hash(params[:variables]),
27
+ context: gql_devise_context(SchemaUser, User)
28
+ )
29
+
30
+ render json: result unless performed?
31
+ end
32
+
22
33
  private
23
34
 
24
35
  def execute_params(item)
@@ -7,17 +7,18 @@ class DummySchema < GraphQL::Schema
7
7
  public_introspection: true,
8
8
  resource_loaders: [
9
9
  GraphqlDevise::ResourceLoader.new(
10
- 'User',
10
+ User,
11
11
  only: [
12
12
  :login,
13
13
  :confirm_account,
14
14
  :send_password_reset,
15
15
  :resend_confirmation,
16
+ :resend_confirmation_with_token,
16
17
  :check_password_token
17
18
  ]
18
19
  ),
19
- GraphqlDevise::ResourceLoader.new('Guest', only: [:logout]),
20
- GraphqlDevise::ResourceLoader.new('SchemaUser')
20
+ GraphqlDevise::ResourceLoader.new(Guest, only: [:logout]),
21
+ GraphqlDevise::ResourceLoader.new(SchemaUser)
21
22
  ]
22
23
  )
23
24
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutations
4
+ class Register < GraphqlDevise::Mutations::Register
5
+ argument :name, String, required: false
6
+
7
+ field :user, Types::UserType, null: true
8
+
9
+ def resolve(email:, **attrs)
10
+ original_payload = super
11
+ original_payload.merge(user: original_payload[:authenticatable])
12
+ end
13
+ end
14
+ end