Skip to content

Commit a0cd5a3

Browse files
Merge pull request #96 from graphql-devise/mount-auth-on-main-schema
Mount auth operations in main GQL schema
2 parents 49d85a1 + 687ebaf commit a0cd5a3

29 files changed

Lines changed: 799 additions & 149 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ README.md.*
1515
/spec/dummy/tmp/
1616
/Gemfile.lock
1717
*.gemfile.lock
18-
/*.sqlite3
18+
*.sqlite3
19+
*.sqlite3-journal
1920
/spec/dummy/db/development.sqlite3
2021
/spec/dummy/db/test.sqlite3
2122
/*.gem

README.md

Lines changed: 187 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,34 @@ GraphQL interface on top of the [Devise Token Auth](https://github.com/lynndylan
1212
* [Table of Contents](#table-of-contents)
1313
* [Introduction](#introduction)
1414
* [Installation](#installation)
15+
* [Running the Generator](#running-the-generator)
16+
* [Mounting the Schema in a Separate Route](#mounting-the-schema-in-a-separate-route)
17+
* [Mounting Operations in Your Own Schema](#mounting-operations-in-your-own-schema)
18+
* [Important](#important)
1519
* [Usage](#usage)
16-
* [Mounting Routes manually](#mounting-routes-manually)
17-
* [Available Operations](#available-operations)
20+
* [Mounting Auth Schema on a Separate Route](#mounting-auth-schema-on-a-separate-route)
21+
* [Mounting Operations Into Your Own Schema](#mounting-operations-into-your-own-schema)
22+
* [Available Mount Options](#available-mount-options)
23+
* [Available Operations](#available-operations)
1824
* [Configuring Model](#configuring-model)
1925
* [Customizing Email Templates](#customizing-email-templates)
2026
* [I18n](#i18n)
2127
* [Authenticating Controller Actions](#authenticating-controller-actions)
28+
* [Authenticate Before Reaching Your GQL Schema](#authenticate-before-reaching-your-gql-schema)
29+
* [Authenticate in Your GQL Schema](#authenticate-in-your-gql-schema)
2230
* [Making Requests](#making-requests)
2331
* [Mutations](#mutations)
2432
* [Queries](#queries)
2533
* [More Configuration Options](#more-configuration-options)
2634
* [Devise Token Auth Initializer](#devise-token-auth-initializer)
2735
* [Devise Initializer](#devise-initializer)
36+
* [GraphQL Interpreter](#graphql-interpreter)
2837
* [Using Alongside Standard Devise](#using-alongside-standard-devise)
2938
* [Future Work](#future-work)
3039
* [Contributing](#contributing)
3140
* [License](#license)
3241

33-
<!-- Added by: mcelicalderon, at: Tue Apr 28 21:43:30 -05 2020 -->
42+
<!-- Added by: mcelicalderon, at: Wed Jun 10 22:10:26 -05 2020 -->
3443

3544
<!--te-->
3645

@@ -54,20 +63,23 @@ gem 'graphql_devise'
5463
```
5564

5665
And then execute:
66+
```bash
67+
$ bundle
68+
```
5769

58-
$ bundle
59-
60-
Next, you need to run the generator:
61-
62-
$ bundle exec rails generate graphql_devise:install
63-
70+
### Running the Generator
6471
Graphql Devise generator will execute `Devise` and `Devise Token Auth`
6572
generators for you. These will make the required changes for the gems to
6673
work correctly. All configurations for [Devise](https://github.com/plataformatec/devise) and
6774
[Devise Token Auth](https://github.com/lynndylanhurley/devise_token_auth) are available,
6875
so you can read the docs there to customize your options.
6976
Configurations are done via initializer files as usual, one per gem.
7077

78+
#### Mounting the Schema in a Separate Route
79+
```bash
80+
$ bundle exec rails generate graphql_devise:install
81+
```
82+
7183
The generator accepts 2 params: `user_class` and `mount_path`. The params
7284
will be used to mount the route in `config/routes.rb`. For instance the executing:
7385

@@ -87,14 +99,32 @@ Will do the following:
8799
`Admin` could be any model name you are going to be using for authentication,
88100
and `api/auth` could be any mount path you would like to use for auth.
89101

90-
**Important:** Remember this gem mounts a completely separate GraphQL schema on a separate controller in the route
102+
#### Mounting Operations in Your Own Schema
103+
Now you can provide to the generator an option specifying
104+
the name of your GQL schema. Doing this will skip the insertion of the mount method in the
105+
routes file and will also add our `SchemaPlugin` to the specified schema. `user_class` param is still optional (`Admin`) in the following example.
106+
107+
```bash
108+
$ bundle exec rails g graphql_devise:install Admin --mount MySchema
109+
```
110+
111+
### Important
112+
Remember that by default this gem mounts a completely separate GraphQL schema on a separate controller in the route
91113
provided by the `at` option in the `mount_graphql_devise_for` method in the `config/routes.rb` file. If no `at`
92-
option is provided, the route will be `/graphql_auth`. This has no effect on your own application schema.
93-
More on this in the next section.
114+
option is provided, the route will be `/graphql_auth`.
115+
116+
**Starting with `v0.12.0`** you can opt-in to load this gem's queries and mutations into your
117+
own application's schema. You can actually mount a resource's auth schema in a separate route
118+
and in your app's schema at the same time, but that's probably not a common scenario. More on
119+
this in the next section.
94120

95121
## Usage
96-
### Mounting Routes manually
97-
Routes can be added using the initializer or manually.
122+
### Mounting Auth Schema on a Separate Route
123+
The generator can do this step for you by default. Remember now you can mount this gem's
124+
auth operations into your own schema as described in [this section](#mounting-operations-into-your-own-schema).
125+
126+
127+
Routes can be added using the generator or manually.
98128
You can mount this gem's GraphQL auth schema in your routes file like this:
99129

100130
```ruby
@@ -120,11 +150,83 @@ Rails.application.routes.draw do
120150
)
121151
end
122152
```
153+
The second argument of the `mount_graphql_devise` method is a hash of options where you can
154+
customize how the queries and mutations are mounted into the schema. For a list of available
155+
options go [here](#available-mount-options)
156+
157+
### Mounting Operations Into Your Own Schema
158+
Starting with `v0.12.0` you can now mount the GQL operations provided by this gem into your
159+
app's main schema.
160+
161+
```ruby
162+
# app/graphql/dummy_schema.rb
163+
164+
class DummySchema < GraphQL::Schema
165+
# It's important that this line goes before setting the query and mutation type on your
166+
# schema in graphql versions < 1.10.0
167+
use GraphqlDevise::SchemaPlugin.new(
168+
query: Types::QueryType,
169+
mutation: Types::MutationType,
170+
resource_loaders: [
171+
GraphqlDevise::ResourceLoader.new('User', only: [:login, :confirm_account])
172+
]
173+
)
174+
175+
mutation(Types::MutationType)
176+
query(Types::QueryType)
177+
end
178+
```
179+
The example above describes just one of the possible scenarios you might need.
180+
The second argument of the `GraphqlDevise::ResourceLoader` initializer is a hash of
181+
options where you can customize how the queries and mutations are mounted into the schema.
182+
For a list of available options go [here](#available-mount-options).
183+
184+
It's important to use the plugin in your schema before assigning the mutation and query type to
185+
it in graphql versions `< 1.10.0`. Otherwise the auth operations won't be available.
186+
187+
You can provide as many resource loaders as you need to the `resource_loaders` option, and each
188+
of those will be loaded into your schema. These are the options you can initialize the
189+
`SchemaPlugin` with:
190+
191+
1. `query`: This param is mandatory unless you skip all queries via the resource loader
192+
options. This should be the same `QueryType` you provide to the `query` method
193+
in your schema.
194+
1. `mutation`: This param mandatory unless you skip all mutations via the resource loader
195+
options. This should be the same `MutationType` you provide to the `mutation` method
196+
in your schema.
197+
1. `resource_loaders`: This is an optional array of `GraphqlDevise::ResourceLoader` instances.
198+
Here is where you specify the operations that you want to load into your app's schema.
199+
If no loader is provided, no operations will be added to your schema, but you will still be
200+
able to authenticate queries and mutations selectively. More on this in the controller
201+
authentication [section](#authenticating-controller-actions).
202+
1. `authenticate_default`: This is a boolean value which is `true` by default. This value
203+
defines what is the default behavior for authentication in your schema fields. `true` means
204+
every root level field requires authentication unless specified otherwise using the
205+
`authenticate: false` option on the field. `false` means your root level fields won't require
206+
authentication unless specified otherwise using the `authenticate: true` option on the field.
207+
1. `unauthenticated_proc`: This param is optional. Here you can provide a proc that receives
208+
one argument (field name) and is called whenever a field that requires authentication
209+
is called without an authenticated resource. By default a `GraphQL::ExecutionError` will be
210+
raised if authentication fails. This will provide a GQL like error message on the response.
211+
212+
### Available Mount Options
213+
Both the `mount_graphql_devise_for` method and the `GraphqlDevise::ResourceLoader` class
214+
take the same options. So, wether you decide to mount this gem in a separate route
215+
from your main application's schema or you use our `GraphqlDevise::SchemaPlugin` to load
216+
this gem's auth operation into your schema, these are the options you can provide as a hash.
123217

124-
Here are the options for the mount method:
218+
```ruby
219+
# Using the mount method in your config/routes.rb file
220+
mount_graphql_devise_for('User', {})
221+
222+
# Providing options to a GraphqlDevise::ResourceLoader
223+
GraphqlDevise::ResourceLoader.new('User', {})
224+
```
125225

126-
1. `at`: Route where the GraphQL schema will be mounted on the Rails server. In this example your API will have these two routes: `POST /api/v1/graphql_auth` and `GET /api/v1/graphql_auth`.
127-
If this option is not specified, the schema will be mounted at `/graphql_auth`.
226+
1. `at`: Route where the GraphQL schema will be mounted on the Rails server.
227+
In [this example](#mounting-auth-schema-on-a-separate-route) your API will have
228+
these two routes: `POST /api/v1/graphql_auth` and `GET /api/v1/graphql_auth`.
229+
If this option is not specified, the schema will be mounted at `/graphql_auth`. **This option only works if you are using the mount method.**
128230
1. `operations`: Specifying this is optional. Here you can override default
129231
behavior by specifying your own mutations and queries for every GraphQL operation.
130232
Check available operations in this file [mutations](https://github.com/graphql-devise/graphql_devise/blob/b5985036e01ea064e43e457b4f0c8516f172471c/lib/graphql_devise/rails/routes.rb#L19)
@@ -163,7 +265,7 @@ or [base resolver](https://github.com/graphql-devise/graphql_devise/blob/master/
163265
respectively, to take advantage of some of the methods provided by devise
164266
just like with `devise_scope`
165267

166-
#### Available Operations
268+
### Available Operations
167269
The following is a list of the symbols you can provide to the `operations`, `skip` and `only` options of the mount method:
168270
```ruby
169271
:login
@@ -175,7 +277,6 @@ The following is a list of the symbols you can provide to the `operations`, `ski
175277
:check_password_token
176278
```
177279

178-
179280
### Configuring Model
180281
Just like with Devise and DTA, you need to include a module in your authenticatable model,
181282
so with our example, your user model will have to look like this:
@@ -216,6 +317,9 @@ Keep in mind that if your app uses multiple locales, you should set the `I18n.lo
216317

217318
### Authenticating Controller Actions
218319
Just like with Devise or DTA, you will need to authenticate users in your controllers.
320+
For this you have two alternatives.
321+
322+
#### Authenticate Before Reaching Your GQL Schema
219323
For this you need to call `authenticate_<model>!` in a before_action hook of your controller.
220324
In our example our model is `User`, so it would look like this:
221325
```ruby
@@ -234,6 +338,62 @@ end
234338

235339
The install generator can do this for you because it executes DTA installer.
236340
See [Installation](#Installation) for details.
341+
If authentication fails for the request for whatever reason, execution of the request is halted
342+
and an error is returned in a REST format as the request never reaches your GQL schema.
343+
344+
#### Authenticate in Your GQL Schema
345+
For this you will need to add the `GraphqlDevise::SchemaPlugin` to your schema as described
346+
[here](#mounting-operations-into-your-own-schema) and also set the authenticated resource
347+
in a `before_action` hook.
348+
349+
```ruby
350+
# app/controllers/my_controller.rb
351+
352+
class MyController < ApplicationController
353+
include GraphqlDevise::Concerns::SetUserByToken
354+
355+
before_action -> { set_resource_by_token(:user) }
356+
357+
def my_action
358+
render json: DummySchema.execute(params[:query], context: graphql_context)
359+
end
360+
end
361+
362+
# @resource.to_s.underscore.tr('/', '_').to_sym
363+
```
364+
The `set_resource_by_token` method receives a symbol identifying the resource you are trying
365+
to authenticate. So if you mounted the `'User'` resource, the symbol is `:user`. You can use
366+
this snippet to find the symbol for more complex scenarios
367+
`resource_klass.to_s.underscore.tr('/', '_').to_sym`.
368+
369+
The `graphql_context` method is simply a helper method that returns a hash like this
370+
```ruby
371+
{ current_resource: @resource, controller: self }
372+
```
373+
These are the two values the gem needs to check if a user is authenticated and to perform
374+
other auth operations. All `set_resource_by_token` does is set the `@resource` variable if
375+
the provided authentication headers are valid. If authentication fails, resource will be `nil`
376+
and this is how `GraphqlDevise::SchemaPlugin` knows if a user is authenticated or not in
377+
each query.
378+
379+
Please note that by using this mechanism your GQL schema will be in control of what queries are
380+
restricted to authenticated users and you can only do this at the root level fields of your GQL
381+
schema. Configure the plugin as explained [here](#mounting-operations-into-your-own-schema)
382+
so this can work.
383+
384+
In you main app's schema this is how you might specify if a field needs to be authenticated or not:
385+
```ruby
386+
module Types
387+
class QueryType < Types::BaseObject
388+
# user field used the default set in the Plugin's initializer
389+
field :user, resolver: Resolvers::UserShow
390+
# this field will never require authentication
391+
field :public_field, String, null: false, authenticate: false
392+
# this field requires authentication
393+
field :private_field, String, null: false, authenticate: true
394+
end
395+
end
396+
```
237397

238398
### Making Requests
239399
Here is a list of the available mutations and queries assuming your mounted model is `User`.
@@ -308,6 +468,14 @@ In this section the most important configurations will be highlighted.
308468

309469
**Note:** Remember this gem adds a layer on top of Devise, so some configurations might not apply.
310470

471+
### GraphQL Interpreter
472+
GraphQL-Ruby `>= 1.9.0` includes a new runtime module which you may use for your schema.
473+
Eventually, it will become the default. You can read more about it
474+
[here](https://graphql-ruby.org/queries/interpreter).
475+
476+
This gem supports schemas using the interpreter and it is recommended as it introduces several
477+
improvements which focus mainly on performance.
478+
311479
### Using Alongside Standard Devise
312480
The DeviseTokenAuth gem allows experimental use of the standard Devise gem to be configured at the same time, for more
313481
information you can check [this answer here](https://github.com/lynndylanhurley/devise_token_auth/blob/2a32f18ccce15638a74e72f6cfde5cf15a808d3f/docs/faq.md#can-i-use-this-gem-alongside-standard-devise).
@@ -318,7 +486,6 @@ standard Devise templates.
318486
## Future Work
319487
We will continue to improve the gem and add better docs.
320488

321-
1. Add mount option that will create a separate schema for the mounted resource.
322489
1. Make sure this gem can correctly work alongside DTA and the original Devise gem.
323490
1. Improve DOCS.
324491
1. Add support for unlockable and other Devise modules.
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
module GraphqlDevise
2-
class ApplicationController < DeviseTokenAuth::ApplicationController
3-
private
4-
5-
def verify_authenticity_token
6-
end
2+
ApplicationController = if Rails::VERSION::MAJOR >= 5
3+
Class.new(ActionController::API)
4+
else
5+
Class.new(ActionController::Base)
76
end
87
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
module GraphqlDevise
22
module Concerns
33
SetUserByToken = DeviseTokenAuth::Concerns::SetUserByToken
4+
5+
SetUserByToken.module_eval do
6+
attr_accessor :client_id, :token, :resource
7+
8+
alias_method :set_resource_by_token, :set_user_by_token
9+
10+
def graphql_context
11+
{
12+
current_resource: @resource,
13+
controller: self
14+
}
15+
end
16+
17+
def build_redirect_headers(access_token, client, redirect_header_options = {})
18+
{
19+
DeviseTokenAuth.headers_names[:"access-token"] => access_token,
20+
DeviseTokenAuth.headers_names[:client] => client,
21+
:config => params[:config],
22+
:client_id => client,
23+
:token => access_token
24+
}.merge(redirect_header_options)
25+
end
26+
end
427
end
528
end

app/controllers/graphql_devise/graphql_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
module GraphqlDevise
44
class GraphqlController < ApplicationController
5+
include GraphqlDevise::Concerns::SetUserByToken
6+
57
def auth
68
result = if params[:_json]
79
GraphqlDevise::Schema.multiplex(

app/helpers/graphql_devise/mailer_helper.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module GraphqlDevise
22
module MailerHelper
33
def confirmation_query(resource_name:, token:, redirect_url:)
4-
name = "#{resource_name.camelize(:lower)}ConfirmAccount"
4+
name = "#{resource_name.underscore.tr('/', '_').camelize(:lower)}ConfirmAccount"
55
raw = <<-GRAPHQL
66
query($token:String!,$redirectUrl:String!){
77
#{name}(confirmationToken:$token,redirectUrl:$redirectUrl){
@@ -17,7 +17,7 @@ def confirmation_query(resource_name:, token:, redirect_url:)
1717
end
1818

1919
def password_reset_query(token:, redirect_url:, resource_name:)
20-
name = "#{resource_name.camelize(:lower)}CheckPasswordToken"
20+
name = "#{resource_name.underscore.tr('/', '_').camelize(:lower)}CheckPasswordToken"
2121
raw = <<-GRAPHQL
2222
query($token:String!,$redirectUrl:String!){
2323
#{name}(resetPasswordToken:$token,redirectUrl:$redirectUrl){

config/routes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@
1212
GraphqlDevise::Schema.query(GraphqlDevise::Types::QueryType)
1313

1414
GraphqlDevise.load_schema
15+
16+
Devise.mailer.helper(GraphqlDevise::MailerHelper)
1517
end
1618
end

0 commit comments

Comments
 (0)