graphql_devise 0.14.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +118 -0
  3. data/Appraisals +39 -5
  4. data/CHANGELOG.md +68 -6
  5. data/README.md +150 -51
  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/docs/usage/reset_password_flow.md +90 -0
  14. data/graphql_devise.gemspec +2 -2
  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/mount_method/operation_preparer.rb +6 -6
  19. data/lib/graphql_devise/mount_method/operation_preparers/custom_operation_preparer.rb +6 -4
  20. data/lib/graphql_devise/mount_method/operation_preparers/default_operation_preparer.rb +6 -4
  21. data/lib/graphql_devise/mount_method/operation_preparers/{resource_name_setter.rb → resource_klass_setter.rb} +4 -4
  22. data/lib/graphql_devise/resolvers/confirm_account.rb +1 -1
  23. data/lib/graphql_devise/resource_loader.rb +26 -11
  24. data/lib/graphql_devise/schema_plugin.rb +41 -18
  25. data/lib/graphql_devise/version.rb +1 -1
  26. data/spec/dummy/app/controllers/api/v1/graphql_controller.rb +13 -2
  27. data/spec/dummy/app/graphql/dummy_schema.rb +4 -3
  28. data/spec/dummy/app/graphql/types/query_type.rb +5 -0
  29. data/spec/dummy/config/routes.rb +2 -1
  30. data/spec/dummy/db/migrate/20200623003142_create_schema_users.rb +0 -1
  31. data/spec/dummy/db/migrate/20210516211417_add_vip_to_users.rb +5 -0
  32. data/spec/dummy/db/schema.rb +4 -4
  33. data/spec/generators/graphql_devise/install_generator_spec.rb +1 -1
  34. data/spec/graphql/user_queries_spec.rb +120 -0
  35. data/spec/requests/graphql_controller_spec.rb +12 -11
  36. data/spec/requests/queries/introspection_query_spec.rb +149 -0
  37. data/spec/requests/user_controller_spec.rb +93 -32
  38. data/spec/services/mount_method/operation_preparer_spec.rb +5 -5
  39. data/spec/services/mount_method/operation_preparers/custom_operation_preparer_spec.rb +5 -5
  40. data/spec/services/mount_method/operation_preparers/default_operation_preparer_spec.rb +5 -5
  41. data/spec/services/mount_method/operation_preparers/{resource_name_setter_spec.rb → resource_klass_setter_spec.rb} +6 -6
  42. data/spec/services/resource_loader_spec.rb +5 -5
  43. data/spec/support/contexts/graphql_request.rb +11 -3
  44. data/spec/support/contexts/schema_test.rb +14 -0
  45. metadata +25 -14
  46. data/.travis.yml +0 -79
data/Rakefile CHANGED
@@ -18,11 +18,12 @@ end
18
18
 
19
19
  require 'github_changelog_generator/task'
20
20
 
21
- GitHubChangelogGenerator::RakeTask.new :changelog do |config|
21
+ GitHubChangelogGenerator::RakeTask.new do |config|
22
22
  config.user = 'graphql-devise'
23
23
  config.project = 'graphql_devise'
24
24
  config.future_release = ENV['FUTURE_RELEASE']
25
25
  config.add_issues_wo_labels = false
26
+ config.add_pr_wo_labels = false
26
27
  end
27
28
 
28
29
  APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlDevise
4
+ module Concerns
5
+ module AdditionalControllerMethods
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_accessor :client_id, :token, :resource
10
+ end
11
+
12
+ def gql_devise_context(*models)
13
+ {
14
+ current_resource: authenticate_model(*models),
15
+ controller: self
16
+ }
17
+ end
18
+
19
+ def authenticate_model(*models)
20
+ models.each do |model|
21
+ set_resource_by_token(model)
22
+ return @resource if @resource.present?
23
+ end
24
+
25
+ nil
26
+ end
27
+
28
+ def resource_class(resource = nil)
29
+ # Return the resource class instead of looking for a Devise mapping if resource is already a resource class
30
+ return resource if resource.respond_to?(:find_by)
31
+
32
+ super
33
+ end
34
+
35
+ def full_url_without_params
36
+ request.base_url + request.path
37
+ end
38
+
39
+ def set_resource_by_token(resource)
40
+ set_user_by_token(resource)
41
+ end
42
+
43
+ def graphql_context(resource_name)
44
+ ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
45
+ `graphql_context` is deprecated and will be removed in a future version of this gem.
46
+ Use `gql_devise_context(model)` instead.
47
+
48
+ EXAMPLE
49
+ include GraphqlDevise::Concerns::SetUserByToken
50
+
51
+ DummySchema.execute(params[:query], context: gql_devise_context(User))
52
+ DummySchema.execute(params[:query], context: gql_devise_context(User, Admin))
53
+ DEPRECATION
54
+
55
+ {
56
+ resource_name: resource_name,
57
+ controller: self
58
+ }
59
+ end
60
+
61
+ def build_redirect_headers(access_token, client, redirect_header_options = {})
62
+ {
63
+ DeviseTokenAuth.headers_names[:"access-token"] => access_token,
64
+ DeviseTokenAuth.headers_names[:client] => client,
65
+ :config => params[:config],
66
+ :client_id => client,
67
+ :token => access_token
68
+ }.merge(redirect_header_options)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -2,34 +2,12 @@
2
2
 
3
3
  module GraphqlDevise
4
4
  module Concerns
5
- SetUserByToken = DeviseTokenAuth::Concerns::SetUserByToken
5
+ module SetUserByToken
6
+ extend ActiveSupport::Concern
6
7
 
7
- SetUserByToken.module_eval do
8
- attr_accessor :client_id, :token, :resource
9
-
10
- def full_url_without_params
11
- request.base_url + request.path
12
- end
13
-
14
- def set_resource_by_token(resource)
15
- set_user_by_token(resource)
16
- end
17
-
18
- def graphql_context(resource_name)
19
- {
20
- resource_name: resource_name,
21
- controller: self
22
- }
23
- end
24
-
25
- def build_redirect_headers(access_token, client, redirect_header_options = {})
26
- {
27
- DeviseTokenAuth.headers_names[:"access-token"] => access_token,
28
- DeviseTokenAuth.headers_names[:client] => client,
29
- :config => params[:config],
30
- :client_id => client,
31
- :token => access_token
32
- }.merge(redirect_header_options)
8
+ included do
9
+ include DeviseTokenAuth::Concerns::SetUserByToken
10
+ include GraphqlDevise::Concerns::AdditionalControllerMethods
33
11
  end
34
12
  end
35
13
  end
@@ -14,7 +14,7 @@ module GraphqlDevise
14
14
  end
15
15
  )
16
16
  else
17
- GraphqlDevise::Schema.execute(params[:query], execute_params(params))
17
+ GraphqlDevise::Schema.execute(params[:query], **execute_params(params))
18
18
  end
19
19
 
20
20
  render json: result unless performed?
@@ -3,7 +3,7 @@
3
3
  module GraphqlDevise
4
4
  module MailerHelper
5
5
  def confirmation_query(resource_name:, token:, redirect_url:)
6
- name = "#{resource_name.underscore.tr('/', '_').camelize(:lower)}ConfirmAccount"
6
+ name = "#{GraphqlDevise.to_mapping_name(resource_name).camelize(:lower)}ConfirmAccount"
7
7
  raw = <<-GRAPHQL
8
8
  query($token:String!,$redirectUrl:String!){
9
9
  #{name}(confirmationToken:$token,redirectUrl:$redirectUrl){
@@ -19,7 +19,7 @@ module GraphqlDevise
19
19
  end
20
20
 
21
21
  def password_reset_query(token:, redirect_url:, resource_name:)
22
- name = "#{resource_name.underscore.tr('/', '_').camelize(:lower)}CheckPasswordToken"
22
+ name = "#{GraphqlDevise.to_mapping_name(resource_name).camelize(:lower)}CheckPasswordToken"
23
23
  raw = <<-GRAPHQL
24
24
  query($token:String!,$redirectUrl:String!){
25
25
  #{name}(resetPasswordToken:$token,redirectUrl:$redirectUrl){
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql_devise/model/with_email_updater'
4
+
5
+ module GraphqlDevise
6
+ module Concerns
7
+ module AdditionalModelMethods
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ def reconfirmable
12
+ devise_modules.include?(:confirmable) && column_names.include?('unconfirmed_email')
13
+ end
14
+ end
15
+
16
+ def update_with_email(attributes = {})
17
+ GraphqlDevise::Model::WithEmailUpdater.new(self, attributes).call
18
+ end
19
+ end
20
+ end
21
+ end
@@ -4,17 +4,14 @@ require 'graphql_devise/model/with_email_updater'
4
4
 
5
5
  module GraphqlDevise
6
6
  module Concerns
7
- Model = DeviseTokenAuth::Concerns::User
7
+ module Model
8
+ extend ActiveSupport::Concern
8
9
 
9
- Model.module_eval do
10
- class_methods do
11
- def reconfirmable
12
- devise_modules.include?(:confirmable) && column_names.include?('unconfirmed_email')
13
- end
14
- end
10
+ included do
11
+ include DeviseTokenAuth::Concerns::User
12
+ include GraphqlDevise::Concerns::AdditionalModelMethods
15
13
 
16
- def update_with_email(attributes = {})
17
- GraphqlDevise::Model::WithEmailUpdater.new(self, attributes).call
14
+ GraphqlDevise.configure_warden_serializer_for_model(self)
18
15
  end
19
16
  end
20
17
  end
@@ -0,0 +1,90 @@
1
+ # Reset Password Flow
2
+ This gem supports two different ways to reset a password on a resource. Each password reset flow has it's own set of
3
+ operations and this document will explain in more detail how to use each.
4
+ The first and most recently implemented flow is preferred as it requires less steps and doesn't require a mutation
5
+ to return a redirect on the response. Flow 2 might be deprecated in the future.
6
+
7
+ ## Flow #1 (Preferred)
8
+ This flow only has two steps. Each step name refers to the operation name you can use in the mount options to skip or override.
9
+
10
+ ### 1. send_password_reset_with_token
11
+ This mutation will send an email to the specified address if it's found on the system. Returns an error if the email is not found. Here's an example assuming the resource used
12
+ for authentication is `User`:
13
+ ```graphql
14
+ mutation {
15
+ userSendPasswordResetWithToken(
16
+ email: "vvega@wallaceinc.com",
17
+ redirectUrl: "https://google.com"
18
+ ) {
19
+ message
20
+ }
21
+ }
22
+ ```
23
+ The email will contain a link to the `redirectUrl` (https://google.com in the example) and append a `reset_password_token` query param. This is the token you will
24
+ need to use in the next step in order to reset the password.
25
+
26
+ ### 2. update_password_with_token
27
+ This mutation uses the token sent on the email to find the resource you are trying to recover.
28
+ All you have to do is send a valid token together with the new password and password confirmation.
29
+ Here's an example assuming the resource used for authentication is `User`:
30
+
31
+ ```graphql
32
+ mutation {
33
+ userUpdatePasswordWithToken(
34
+ resetPasswordToken: "token_here",
35
+ password: "password123",
36
+ passwordConfirmation: "password123"
37
+ ) {
38
+ authenticatable { email }
39
+ credentials { accessToken }
40
+ }
41
+ }
42
+ ```
43
+ The mutation has two fields:
44
+ 1. `authenticatable`: Just like other mutations, returns the actual resource you just recover the password for.
45
+ 1. `credentials`: This is a nullable field. It will only return credentials as if you had just logged
46
+ in into the app if you explicitly say so by overriding the mutation. The docs have more detail
47
+ on how to extend the default behavior of mutations, but
48
+ [here](https://github.com/graphql-devise/graphql_devise/blob/8c7c8a5ff1b35fb026e4c9499c70dc5f90b9187a/spec/dummy/app/graphql/mutations/reset_admin_password_with_token.rb)
49
+ you can find an example mutation on what needs to be done in order for the mutation to return
50
+ credentials after updating the password.
51
+
52
+ ## Flow 2 (Deprecated)
53
+ This was the first flow to be implemented, requires an additional step and also to encode a GQL query in a url, so this is not the preferred method.
54
+ Each step name refers to the operation name you can use in the mount options to skip or override.
55
+
56
+ ### 1. send_password_reset
57
+ This mutation will send an email to the specified address if it's found on the system. Returns an error if the email is not found. Here's an example assuming the resource used
58
+ for authentication is `User`:
59
+ ```graphql
60
+ mutation {
61
+ userSendPasswordReset(
62
+ email: "vvega@wallaceinc.com",
63
+ redirectUrl: "https://google.com"
64
+ ) {
65
+ message
66
+ }
67
+ }
68
+ ```
69
+ The email will contain an encoded GraphQL query that holds the reset token and redirectUrl.
70
+ The query is described in the next step.
71
+
72
+ ### 2. check_password_token
73
+ This query checks the reset password token and if successful changes a column in the DB (`allow_password_change`) to true.
74
+ This change will allow for the next step to update the password without providing the current password.
75
+ Then, this query will redirect to the provided `redirectUrl` with credentials.
76
+
77
+ ### 3. update_password
78
+ This step requires the request to include authentication headers and will allow the user to
79
+ update the password if step 2 was successful.
80
+ Here's an example assuming the resource used for authentication is `User`:
81
+ ```graphql
82
+ mutation {
83
+ userUpdatePassword(
84
+ password: "password123",
85
+ passwordConfirmation: "password123"
86
+ ) {
87
+ authenticatable { email }
88
+ }
89
+ }
90
+ ```
@@ -28,11 +28,11 @@ Gem::Specification.new do |spec|
28
28
  spec.required_ruby_version = '>= 2.2.0'
29
29
 
30
30
  spec.add_dependency 'devise_token_auth', '>= 0.1.43', '< 2.0'
31
- spec.add_dependency 'graphql', '>= 1.8', '< 1.12.0'
31
+ spec.add_dependency 'graphql', '>= 1.8', '< 1.13.0'
32
32
  spec.add_dependency 'rails', '>= 4.2', '< 6.2'
33
33
 
34
34
  spec.add_development_dependency 'appraisal'
35
- spec.add_development_dependency 'coveralls'
35
+ spec.add_development_dependency 'coveralls-ruby', '~> 0.2'
36
36
  spec.add_development_dependency 'factory_bot'
37
37
  spec.add_development_dependency 'faker'
38
38
  spec.add_development_dependency 'generator_spec'
@@ -83,7 +83,7 @@ module GraphqlDevise
83
83
  query: Types::QueryType,
84
84
  mutation: Types::MutationType,
85
85
  resource_loaders: [
86
- GraphqlDevise::ResourceLoader.new('#{user_class}'),
86
+ GraphqlDevise::ResourceLoader.new(#{user_class})
87
87
  ]
88
88
  )
89
89
  RUBY
@@ -20,22 +20,36 @@ module GraphqlDevise
20
20
  @schema_loaded = true
21
21
  end
22
22
 
23
- def self.resource_mounted?(mapping_name)
24
- @mounted_resources.include?(mapping_name)
23
+ def self.resource_mounted?(model)
24
+ @mounted_resources.include?(model)
25
25
  end
26
26
 
27
- def self.mount_resource(mapping_name)
28
- @mounted_resources << mapping_name
27
+ def self.mount_resource(model)
28
+ @mounted_resources << model
29
29
  end
30
30
 
31
31
  def self.add_mapping(mapping_name, resource)
32
- return if Devise.mappings.key?(mapping_name)
32
+ return if Devise.mappings.key?(mapping_name.to_sym)
33
33
 
34
34
  Devise.add_mapping(
35
35
  mapping_name.to_s.pluralize.to_sym,
36
- module: :devise, class_name: resource
36
+ module: :devise, class_name: resource.to_s
37
37
  )
38
38
  end
39
+
40
+ def self.to_mapping_name(resource)
41
+ resource.to_s.underscore.tr('/', '_')
42
+ end
43
+
44
+ def self.configure_warden_serializer_for_model(model)
45
+ Devise.warden_config.serialize_into_session(to_mapping_name(model)) do |record|
46
+ model.serialize_into_session(record)
47
+ end
48
+
49
+ Devise.warden_config.serialize_from_session(to_mapping_name(model)) do |args|
50
+ model.serialize_from_session(*args)
51
+ end
52
+ end
39
53
  end
40
54
 
41
55
  require 'graphql_devise/engine'
@@ -40,11 +40,11 @@ module GraphqlDevise
40
40
  end
41
41
 
42
42
  def resource_name
43
- self.class.instance_variable_get(:@resource_name)
43
+ GraphqlDevise.to_mapping_name(resource_class)
44
44
  end
45
45
 
46
46
  def resource_class
47
- controller.send(:resource_class, resource_name)
47
+ self.class.instance_variable_get(:@resource_klass)
48
48
  end
49
49
 
50
50
  def recoverable_enabled?
@@ -60,7 +60,7 @@ module GraphqlDevise
60
60
  end
61
61
 
62
62
  def current_resource
63
- @current_resource ||= controller.send(:set_user_by_token, resource_name)
63
+ @current_resource ||= controller.send(:set_resource_by_token, resource_class)
64
64
  end
65
65
 
66
66
  def client
@@ -3,17 +3,17 @@
3
3
  require_relative 'operation_preparers/gql_name_setter'
4
4
  require_relative 'operation_preparers/mutation_field_setter'
5
5
  require_relative 'operation_preparers/resolver_type_setter'
6
- require_relative 'operation_preparers/resource_name_setter'
6
+ require_relative 'operation_preparers/resource_klass_setter'
7
7
  require_relative 'operation_preparers/default_operation_preparer'
8
8
  require_relative 'operation_preparers/custom_operation_preparer'
9
9
 
10
10
  module GraphqlDevise
11
11
  module MountMethod
12
12
  class OperationPreparer
13
- def initialize(mapping_name:, selected_operations:, preparer:, custom:, additional_operations:)
13
+ def initialize(model:, selected_operations:, preparer:, custom:, additional_operations:)
14
14
  @selected_operations = selected_operations
15
15
  @preparer = preparer
16
- @mapping_name = mapping_name
16
+ @model = model
17
17
  @custom = custom
18
18
  @additional_operations = additional_operations
19
19
  end
@@ -22,18 +22,18 @@ module GraphqlDevise
22
22
  default_operations = OperationPreparers::DefaultOperationPreparer.new(
23
23
  selected_operations: @selected_operations,
24
24
  custom_keys: @custom.keys,
25
- mapping_name: @mapping_name,
25
+ model: @model,
26
26
  preparer: @preparer
27
27
  ).call
28
28
 
29
29
  custom_operations = OperationPreparers::CustomOperationPreparer.new(
30
30
  selected_keys: @selected_operations.keys,
31
31
  custom_operations: @custom,
32
- mapping_name: @mapping_name
32
+ model: @model
33
33
  ).call
34
34
 
35
35
  additional_operations = @additional_operations.each_with_object({}) do |(action, operation), result|
36
- result[action] = OperationPreparers::ResourceNameSetter.new(@mapping_name).call(operation)
36
+ result[action] = OperationPreparers::ResourceKlassSetter.new(@model).call(operation)
37
37
  end
38
38
 
39
39
  default_operations.merge(custom_operations).merge(additional_operations)
@@ -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