graphql_devise 0.13.4 → 0.14.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.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.gitlab-ci.yml +109 -0
- data/Appraisals +39 -5
- data/CHANGELOG.md +47 -0
- data/README.md +20 -9
- data/app/views/graphql_devise/mailer/reset_password_instructions.html.erb +7 -1
- data/config/locales/en.yml +2 -1
- data/docs/usage/reset_password_flow.md +90 -0
- data/graphql_devise.gemspec +1 -1
- data/lib/graphql_devise/concerns/controller_methods.rb +7 -1
- data/lib/graphql_devise/default_operations/mutations.rb +10 -6
- data/lib/graphql_devise/mutations/resend_confirmation.rb +2 -0
- data/lib/graphql_devise/mutations/send_password_reset.rb +2 -0
- data/lib/graphql_devise/mutations/send_password_reset_with_token.rb +37 -0
- data/lib/graphql_devise/mutations/sign_up.rb +1 -3
- data/lib/graphql_devise/mutations/update_password_with_token.rb +38 -0
- data/lib/graphql_devise/resolvers/check_password_token.rb +1 -0
- data/lib/graphql_devise/resolvers/confirm_account.rb +2 -0
- data/lib/graphql_devise/schema_plugin.rb +22 -11
- data/lib/graphql_devise/version.rb +1 -1
- data/spec/dummy/app/graphql/dummy_schema.rb +4 -3
- data/spec/dummy/app/graphql/mutations/reset_admin_password_with_token.rb +13 -0
- data/spec/dummy/config/initializers/devise_token_auth.rb +2 -0
- data/spec/dummy/config/routes.rb +2 -1
- data/spec/graphql/user_queries_spec.rb +118 -0
- data/spec/requests/mutations/additional_mutations_spec.rb +0 -1
- data/spec/requests/mutations/resend_confirmation_spec.rb +16 -1
- data/spec/requests/mutations/send_password_reset_spec.rb +16 -1
- data/spec/requests/mutations/send_password_reset_with_token_spec.rb +78 -0
- data/spec/requests/mutations/sign_up_spec.rb +19 -1
- data/spec/requests/mutations/update_password_with_token_spec.rb +119 -0
- data/spec/requests/queries/check_password_token_spec.rb +16 -1
- data/spec/requests/queries/confirm_account_spec.rb +17 -2
- data/spec/requests/queries/introspection_query_spec.rb +149 -0
- data/spec/requests/user_controller_spec.rb +9 -9
- data/spec/support/contexts/schema_test.rb +14 -0
- metadata +25 -9
- data/.travis.yml +0 -79
data/graphql_devise.gemspec
CHANGED
@@ -28,7 +28,7 @@ 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.
|
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'
|
@@ -7,6 +7,12 @@ module GraphqlDevise
|
|
7
7
|
|
8
8
|
private
|
9
9
|
|
10
|
+
def check_redirect_url_whitelist!(redirect_url)
|
11
|
+
if blacklisted_redirect_url?(redirect_url)
|
12
|
+
raise_user_error(I18n.t('graphql_devise.redirect_url_not_allowed', redirect_url: redirect_url))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
10
16
|
def raise_user_error(message)
|
11
17
|
raise GraphqlDevise::UserError, message
|
12
18
|
end
|
@@ -90,7 +96,7 @@ module GraphqlDevise
|
|
90
96
|
end
|
91
97
|
|
92
98
|
def find_resource(field, value)
|
93
|
-
if resource_class.
|
99
|
+
if resource_class.connection.adapter_name.downcase.include?('mysql')
|
94
100
|
# fix for mysql default case insensitivity
|
95
101
|
resource_class.where("BINARY #{field} = ? AND provider= ?", value, provider).first
|
96
102
|
elsif Gem::Version.new(DeviseTokenAuth::VERSION) < Gem::Version.new('1.1.0')
|
@@ -5,18 +5,22 @@ require 'graphql_devise/mutations/login'
|
|
5
5
|
require 'graphql_devise/mutations/logout'
|
6
6
|
require 'graphql_devise/mutations/resend_confirmation'
|
7
7
|
require 'graphql_devise/mutations/send_password_reset'
|
8
|
+
require 'graphql_devise/mutations/send_password_reset_with_token'
|
8
9
|
require 'graphql_devise/mutations/sign_up'
|
9
10
|
require 'graphql_devise/mutations/update_password'
|
11
|
+
require 'graphql_devise/mutations/update_password_with_token'
|
10
12
|
|
11
13
|
module GraphqlDevise
|
12
14
|
module DefaultOperations
|
13
15
|
MUTATIONS = {
|
14
|
-
login:
|
15
|
-
logout:
|
16
|
-
sign_up:
|
17
|
-
update_password:
|
18
|
-
|
19
|
-
|
16
|
+
login: { klass: GraphqlDevise::Mutations::Login, authenticatable: true },
|
17
|
+
logout: { klass: GraphqlDevise::Mutations::Logout, authenticatable: true },
|
18
|
+
sign_up: { klass: GraphqlDevise::Mutations::SignUp, authenticatable: true },
|
19
|
+
update_password: { klass: GraphqlDevise::Mutations::UpdatePassword, authenticatable: true },
|
20
|
+
update_password_with_token: { klass: GraphqlDevise::Mutations::UpdatePasswordWithToken, authenticatable: true },
|
21
|
+
send_password_reset: { klass: GraphqlDevise::Mutations::SendPasswordReset, authenticatable: false },
|
22
|
+
send_password_reset_with_token: { klass: GraphqlDevise::Mutations::SendPasswordResetWithToken, authenticatable: false },
|
23
|
+
resend_confirmation: { klass: GraphqlDevise::Mutations::ResendConfirmation, authenticatable: false }
|
20
24
|
}.freeze
|
21
25
|
end
|
22
26
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlDevise
|
4
|
+
module Mutations
|
5
|
+
class SendPasswordResetWithToken < Base
|
6
|
+
argument :email, String, required: true
|
7
|
+
argument :redirect_url, String, required: true
|
8
|
+
|
9
|
+
field :message, String, null: false
|
10
|
+
|
11
|
+
def resolve(email:, redirect_url:)
|
12
|
+
check_redirect_url_whitelist!(redirect_url)
|
13
|
+
|
14
|
+
resource = find_resource(:email, get_case_insensitive_field(:email, email))
|
15
|
+
|
16
|
+
if resource
|
17
|
+
yield resource if block_given?
|
18
|
+
|
19
|
+
resource.send_reset_password_instructions(
|
20
|
+
email: email,
|
21
|
+
provider: 'email',
|
22
|
+
redirect_url: redirect_url,
|
23
|
+
template_path: ['graphql_devise/mailer']
|
24
|
+
)
|
25
|
+
|
26
|
+
if resource.errors.empty?
|
27
|
+
{ message: I18n.t('graphql_devise.passwords.send_instructions') }
|
28
|
+
else
|
29
|
+
raise_user_error_list(I18n.t('graphql_devise.invalid_resource'), errors: resource.errors.full_messages)
|
30
|
+
end
|
31
|
+
else
|
32
|
+
raise_user_error(I18n.t('graphql_devise.user_not_found'))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -22,9 +22,7 @@ module GraphqlDevise
|
|
22
22
|
raise_user_error(I18n.t('graphql_devise.registrations.missing_confirm_redirect_url'))
|
23
23
|
end
|
24
24
|
|
25
|
-
|
26
|
-
raise_user_error(I18n.t('graphql_devise.registrations.redirect_url_not_allowed', redirect_url: redirect_url))
|
27
|
-
end
|
25
|
+
check_redirect_url_whitelist!(redirect_url)
|
28
26
|
|
29
27
|
resource.skip_confirmation_notification! if resource.respond_to?(:skip_confirmation_notification!)
|
30
28
|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlDevise
|
4
|
+
module Mutations
|
5
|
+
class UpdatePasswordWithToken < Base
|
6
|
+
argument :password, String, required: true
|
7
|
+
argument :password_confirmation, String, required: true
|
8
|
+
argument :reset_password_token, String, required: true
|
9
|
+
|
10
|
+
field :credentials,
|
11
|
+
GraphqlDevise::Types::CredentialType,
|
12
|
+
null: true,
|
13
|
+
description: 'Authentication credentials. Resource must be signed_in for credentials to be returned.'
|
14
|
+
|
15
|
+
def resolve(reset_password_token:, **attrs)
|
16
|
+
raise_user_error(I18n.t('graphql_devise.passwords.password_recovery_disabled')) unless recoverable_enabled?
|
17
|
+
|
18
|
+
resource = resource_class.with_reset_password_token(reset_password_token)
|
19
|
+
raise_user_error(I18n.t('graphql_devise.passwords.reset_token_not_found')) if resource.blank?
|
20
|
+
raise_user_error(I18n.t('graphql_devise.passwords.reset_token_expired')) unless resource.reset_password_period_valid?
|
21
|
+
|
22
|
+
if resource.update(attrs)
|
23
|
+
yield resource if block_given?
|
24
|
+
|
25
|
+
response_payload = { authenticatable: resource }
|
26
|
+
response_payload[:credentials] = set_auth_headers(resource) if controller.signed_in?(resource_name)
|
27
|
+
|
28
|
+
response_payload
|
29
|
+
else
|
30
|
+
raise_user_error_list(
|
31
|
+
I18n.t('graphql_devise.passwords.update_password_error'),
|
32
|
+
errors: resource.errors.full_messages
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -7,6 +7,8 @@ module GraphqlDevise
|
|
7
7
|
argument :redirect_url, String, required: true
|
8
8
|
|
9
9
|
def resolve(confirmation_token:, redirect_url:)
|
10
|
+
check_redirect_url_whitelist!(redirect_url)
|
11
|
+
|
10
12
|
resource = resource_class.confirm_by_token(confirmation_token)
|
11
13
|
|
12
14
|
if resource.errors.empty?
|
@@ -2,13 +2,16 @@
|
|
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
|
@@ -24,13 +27,12 @@ module GraphqlDevise
|
|
24
27
|
# Authenticate only root level queries
|
25
28
|
return yield unless event == 'execute_field' && path(trace_data).count == 1
|
26
29
|
|
27
|
-
field
|
28
|
-
|
29
|
-
context
|
30
|
+
field = traced_field(trace_data)
|
31
|
+
auth_required = authenticate_option(field, trace_data)
|
32
|
+
context = context_from_data(trace_data)
|
30
33
|
|
31
|
-
if !
|
32
|
-
|
33
|
-
elsif @authenticate_default
|
34
|
+
if auth_required && !(public_introspection && introspection_field?(field))
|
35
|
+
context = set_current_resource(context)
|
34
36
|
raise_on_missing_resource(context, field)
|
35
37
|
end
|
36
38
|
|
@@ -39,10 +41,13 @@ module GraphqlDevise
|
|
39
41
|
|
40
42
|
private
|
41
43
|
|
44
|
+
attr_reader :public_introspection
|
45
|
+
|
42
46
|
def set_current_resource(context)
|
43
|
-
controller
|
44
|
-
resource_names
|
45
|
-
|
47
|
+
controller = context[:controller]
|
48
|
+
resource_names = Array(context[:resource_name])
|
49
|
+
|
50
|
+
context[:current_resource] ||= resource_names.find do |resource_name|
|
46
51
|
unless Devise.mappings.key?(resource_name)
|
47
52
|
raise(
|
48
53
|
GraphqlDevise::Error,
|
@@ -88,11 +93,13 @@ module GraphqlDevise
|
|
88
93
|
end
|
89
94
|
|
90
95
|
def authenticate_option(field, trace_data)
|
91
|
-
if trace_data[:context]
|
96
|
+
auth_required = if trace_data[:context]
|
92
97
|
field.metadata[:authenticate]
|
93
98
|
else
|
94
99
|
field.graphql_definition.metadata[:authenticate]
|
95
100
|
end
|
101
|
+
|
102
|
+
auth_required.nil? ? @authenticate_default : auth_required
|
96
103
|
end
|
97
104
|
|
98
105
|
def reconfigure_warden!
|
@@ -107,6 +114,10 @@ module GraphqlDevise
|
|
107
114
|
resource_loader.call(@query, @mutation)
|
108
115
|
end
|
109
116
|
end
|
117
|
+
|
118
|
+
def introspection_field?(field)
|
119
|
+
INTROSPECTION_FIELDS.include?(field.name)
|
120
|
+
end
|
110
121
|
end
|
111
122
|
end
|
112
123
|
|
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
class DummySchema < GraphQL::Schema
|
4
4
|
use GraphqlDevise::SchemaPlugin.new(
|
5
|
-
query:
|
6
|
-
mutation:
|
7
|
-
|
5
|
+
query: Types::QueryType,
|
6
|
+
mutation: Types::MutationType,
|
7
|
+
public_introspection: true,
|
8
|
+
resource_loaders: [
|
8
9
|
GraphqlDevise::ResourceLoader.new(
|
9
10
|
'User',
|
10
11
|
only: [
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutations
|
4
|
+
class ResetAdminPasswordWithToken < GraphqlDevise::Mutations::UpdatePasswordWithToken
|
5
|
+
field :authenticatable, Types::AdminType, null: false
|
6
|
+
|
7
|
+
def resolve(reset_password_token:, **attrs)
|
8
|
+
super do |admin|
|
9
|
+
controller.sign_in(admin)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -39,6 +39,8 @@ DeviseTokenAuth.setup do |config|
|
|
39
39
|
|
40
40
|
config.default_confirm_success_url = 'https://google.com'
|
41
41
|
|
42
|
+
config.redirect_whitelist = ['https://google.com']
|
43
|
+
|
42
44
|
# By default we will use callbacks for single omniauth.
|
43
45
|
# It depends on fields like email, provider and uid.
|
44
46
|
# config.default_callbacks = true
|
data/spec/dummy/config/routes.rb
CHANGED
@@ -15,7 +15,8 @@ Rails.application.routes.draw do
|
|
15
15
|
authenticatable_type: Types::CustomAdminType,
|
16
16
|
skip: [:sign_up, :check_password_token],
|
17
17
|
operations: {
|
18
|
-
confirm_account:
|
18
|
+
confirm_account: Resolvers::ConfirmAdminAccount,
|
19
|
+
update_password_with_token: Mutations::ResetAdminPasswordWithToken
|
19
20
|
},
|
20
21
|
at: '/api/v1/admin/graphql_auth'
|
21
22
|
)
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'Users controller specs' do
|
6
|
+
include_context 'with graphql schema test'
|
7
|
+
|
8
|
+
let(:schema) { DummySchema }
|
9
|
+
let(:user) { create(:user, :confirmed) }
|
10
|
+
let(:field) { 'privateField' }
|
11
|
+
let(:public_message) { 'Field does not require authentication' }
|
12
|
+
let(:private_message) { 'Field will always require authentication' }
|
13
|
+
let(:private_error) do
|
14
|
+
{
|
15
|
+
message: "#{field} field requires authentication",
|
16
|
+
extensions: { code: 'AUTHENTICATION_ERROR' }
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'publicField' do
|
21
|
+
let(:query) do
|
22
|
+
<<-GRAPHQL
|
23
|
+
query {
|
24
|
+
publicField
|
25
|
+
}
|
26
|
+
GRAPHQL
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'when using a regular schema' do
|
30
|
+
it 'does not require authentication' do
|
31
|
+
expect(response[:data][:publicField]).to eq(public_message)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'privateField' do
|
37
|
+
let(:query) do
|
38
|
+
<<-GRAPHQL
|
39
|
+
query {
|
40
|
+
privateField
|
41
|
+
}
|
42
|
+
GRAPHQL
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when using a regular schema' do
|
46
|
+
context 'when user is authenticated' do
|
47
|
+
let(:resource) { user }
|
48
|
+
|
49
|
+
it 'allows to perform the query' do
|
50
|
+
expect(response[:data][:privateField]).to eq(private_message)
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when using a SchemaUser' do
|
54
|
+
let(:resource) { create(:schema_user, :confirmed) }
|
55
|
+
|
56
|
+
it 'allows to perform the query' do
|
57
|
+
expect(response[:data][:privateField]).to eq(private_message)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'when using an interpreter schema' do
|
64
|
+
let(:schema) { InterpreterSchema }
|
65
|
+
|
66
|
+
context 'when user is authenticated' do
|
67
|
+
let(:resource) { user }
|
68
|
+
|
69
|
+
it 'allows to perform the query' do
|
70
|
+
expect(response[:data][:privateField]).to eq(private_message)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'user' do
|
77
|
+
let(:user_data) { { email: user.email, id: user.id } }
|
78
|
+
let(:query) do
|
79
|
+
<<-GRAPHQL
|
80
|
+
query {
|
81
|
+
user(id: #{user.id}) {
|
82
|
+
id
|
83
|
+
email
|
84
|
+
}
|
85
|
+
}
|
86
|
+
GRAPHQL
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'when using a regular schema' do
|
90
|
+
context 'when user is authenticated' do
|
91
|
+
let(:resource) { user }
|
92
|
+
|
93
|
+
it 'allows to perform the query' do
|
94
|
+
expect(response[:data][:user]).to match(**user_data)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'when using an interpreter schema' do
|
100
|
+
let(:schema) { InterpreterSchema }
|
101
|
+
|
102
|
+
context 'when user is authenticated' do
|
103
|
+
let(:resource) { user }
|
104
|
+
|
105
|
+
it 'allows to perform the query' do
|
106
|
+
expect(response[:data][:user]).to match(**user_data)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'when user is not authenticated' do
|
111
|
+
# Interpreter schema fields are public unless specified otherwise (plugin setting)
|
112
|
+
it 'allows to perform the query' do
|
113
|
+
expect(response[:data][:user]).to match(**user_data)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|