graphql-auth 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f94c2a56e51599f277494d890c6b754e7f34933d
4
+ data.tar.gz: 8b2c569822bd5f09286f68bb240449ecb50ae600
5
+ SHA512:
6
+ metadata.gz: a3f06d11726fb5538b0cf38b620e4ac7ef59d981412a98730d2baa0fb1faf6cb579f1d9600c5100422e4c26f6f838aa470e223eabc8a8432c1c3589a00c5cb9a
7
+ data.tar.gz: 9d5ff6f5d125b4afb3673834097aaf287bae87e836c2b4a8cbb483fb7f0491b9cc5cda71f029c4bb2b7f95b3d462d9b709c267ce1519061e1c5e1e2ab1a83a1d
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in graphql-devise-auth.gemspec
6
+ gemspec
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # GraphQL Auth
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/graphql-auth`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'graphql-auth'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install graphql-auth
20
+
21
+ Then run the installer to create `graphql_auth.rb` file in your initializers folder.
22
+
23
+ ```
24
+ rails g graphql_auth:install
25
+ ```
26
+
27
+ Make sure to read all configurations present inside the file and fill them with your own configs.
28
+
29
+ ## Usage
30
+
31
+ Make 'JWT_SECRET_KEY' and 'APP_URL' available to ENV
32
+
33
+ ```
34
+ JWT_SECRET_KEY=
35
+ APP_URL=
36
+ ```
37
+
38
+ Make sure the `Authorization` header is allowed in your api
39
+
40
+ ```
41
+ Rails.application.config.middleware.insert_before 0, Rack::Cors do
42
+ allow do
43
+ origins '*'
44
+ resource '*',
45
+ headers: %w(Authorization),
46
+ methods: :any,
47
+ expose: %w(Authorization),
48
+ max_age: 600
49
+ end
50
+ end
51
+ ```
52
+
53
+ Make sure to include `Graphql::AuthHelper` in your `GraphqlController`. A context method returning the current_user will be available
54
+
55
+ ```
56
+ class GraphqlController < ActionController::API
57
+
58
+ include Graphql::AuthHelper
59
+
60
+ def execute
61
+ variables = ensure_hash(params[:variables])
62
+ query = params[:query]
63
+ operation_name = params[:operationName]
64
+ result = ::GraphqlSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
65
+ render json: result
66
+
67
+ ...
68
+ ```
69
+
70
+ Make sure to implement `GraphqlAuth` in your MutationType``to make auth mutations available
71
+
72
+ ```
73
+ class Types::MutationType < Types::BaseObject
74
+ implements GraphqlAuth
75
+ end
76
+ ```
77
+
78
+ ## Customization
79
+
80
+ If you can to customize any mutation, make sure to update the configurations
81
+
82
+ ```
83
+ GraphQL::Auth.configure do |config|
84
+ # config.token_lifespan = 4.hours
85
+ # config.jwt_secret_key = ENV['JWT_SECRET_KEY']
86
+ # config.app_url = ENV['APP_URL']
87
+
88
+ config.sign_in_mutation = ::Mutations::CustomSignIn
89
+
90
+ ...
91
+
92
+ ```
93
+
94
+ ## Development
95
+
96
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
97
+
98
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `graphql-auth.gemspec`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
99
+
100
+ ## Contributing
101
+
102
+ Bug reports and pull requests are welcome on GitHub at https://github.com/o2web/graphql-auth. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
103
+
104
+ ## License
105
+
106
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
107
+
108
+ ## Code of Conduct
109
+
110
+ Everyone interacting in the GraphQL Auth project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/graphql-devise-auth/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # mutation {
4
+ # forgotPassword(email: "email@example.com") {
5
+ # valid
6
+ # }
7
+ # }
8
+
9
+ class Mutations::ForgotPassword < GraphQL::Schema::Mutation
10
+ argument :email, String, required: true do
11
+ description 'The email with forgotten password'
12
+ end
13
+
14
+ field :valid, Boolean, null: false
15
+
16
+ def resolve(email:)
17
+ user = User.find_by email: email
18
+ user.send_reset_password_instructions if user.present?
19
+
20
+ { valid: true }
21
+ end
22
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # mutation {
4
+ # resetPassword(resetPasswordToken: "token", password: "password", passwordConfirmation: "password") {
5
+ # valid
6
+ # errors {
7
+ # field
8
+ # message
9
+ # }
10
+ # }
11
+ # }
12
+
13
+ class Mutations::ResetPassword < GraphQL::Schema::Mutation
14
+ argument :reset_password_token, String, required: true do
15
+ description "Reset password token"
16
+ end
17
+
18
+ argument :password, String, required: true do
19
+ description "New user's new password"
20
+ end
21
+
22
+ argument :password_confirmation, String, required: true do
23
+ description "New user's new password confirmation"
24
+ end
25
+
26
+ field :valid, Boolean, null: false
27
+ field :errors, [Types::Error], null: true
28
+
29
+ def resolve(args)
30
+ user = User.reset_password_by_token args
31
+
32
+ if user.errors.any?
33
+ {
34
+ valid: false,
35
+ errors: user.errors.messages.map do |field, messages|
36
+ field = field == :reset_password_token ? :_error : field.to_s.camelize(:lower)
37
+ {
38
+ field: field,
39
+ message: messages.first.capitalize }
40
+ end
41
+ }
42
+ else
43
+ { valid: true }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # mutation {
4
+ # signIn(email: "email@example.com", password: "password") {
5
+ # user {
6
+ # email
7
+ # }
8
+ # errors {
9
+ # field
10
+ # message
11
+ # }
12
+ # }
13
+ # }
14
+
15
+ class Mutations::SignIn < GraphQL::Schema::Mutation
16
+ argument :email, String, required: true do
17
+ description "The user's email"
18
+ end
19
+
20
+ argument :password, String, required: true do
21
+ description "The user's password"
22
+ end
23
+
24
+ field :user, Types::User, null: true
25
+ field :errors, [Types::Error], null: true
26
+
27
+ def resolve(email:, password:)
28
+ response = context[:response]
29
+ user = User.find_by email: email
30
+ valid_sign_in = user.present? && user.valid_password?(password)
31
+
32
+ if valid_sign_in
33
+ response.set_header 'Authorization', GraphQL::Auth::JwtManager.issue({ user: user.id }) # TODO use uuid
34
+ {
35
+ user: user
36
+ }
37
+ else
38
+ {
39
+ errors: [
40
+ {
41
+ field: :_error,
42
+ message: I18n.t('devise.failure.invalid',
43
+ authentication_keys: I18n.t('activerecord.attributes.user.email'))
44
+ }
45
+ ]
46
+ }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # mutation {
4
+ # signUp(email: "email@example.com", password: "password", passwordConfirmation: "password") {
5
+ # user {
6
+ # email
7
+ # }
8
+ # errors {
9
+ # field
10
+ # message
11
+ # }
12
+ # }
13
+ # }
14
+
15
+ class Mutations::SignUp < GraphQL::Schema::Mutation
16
+ argument :email, String, required: true do
17
+ description "New user's email"
18
+ end
19
+
20
+ argument :password, String, required: true do
21
+ description "New user's password"
22
+ end
23
+
24
+ argument :password_confirmation, String, required: true do
25
+ description "New user's password confirmation"
26
+ end
27
+
28
+ field :user, Types::User, null: true
29
+ field :errors, [Types::Error], null: true
30
+
31
+ def resolve(args)
32
+ response = context[:response]
33
+ user = User.new args
34
+
35
+ if user.save
36
+ response.set_header 'Authorization', GraphQL::Auth::JwtManager.issue({ user: user.id }) # TODO use uuid
37
+ { user: user }
38
+ else
39
+ {
40
+ errors: user.errors.messages.map do |field, messages|
41
+ { field: field.to_s.camelize(:lower), message: messages.first.capitalize }
42
+ end
43
+ }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # mutation {
4
+ # updateAccount(current_password: "currentPassword", password: "newPassword", password_confirmation: "newPassword") {
5
+ # user {
6
+ # email
7
+ # }
8
+ # errors {
9
+ # field
10
+ # message
11
+ # }
12
+ # }
13
+ # }
14
+
15
+ class Mutations::UpdateAccount < GraphQL::Schema::Mutation
16
+ argument :current_password, String, required: true do
17
+ description "User's current password"
18
+ end
19
+
20
+ argument :password, String, required: true do
21
+ description "User's new password"
22
+ end
23
+
24
+ argument :password_confirmation, String, required: true do
25
+ description "User's new password confirmation"
26
+ end
27
+
28
+ field :user, Types::User, null: true
29
+ field :errors, [Types::Error], null: true
30
+
31
+ def resolve(args)
32
+ user = context[:current_user]
33
+ user.update_with_password args
34
+
35
+ if user.errors.any?
36
+ {
37
+ errors: user.errors.messages.map do |field, messages|
38
+ { field: field.to_s.camelize(:lower), message: messages.first.capitalize }
39
+ end
40
+ }
41
+ else
42
+ { user: user }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # mutation {
4
+ # validateToken {
5
+ # valid
6
+ # user {
7
+ # email
8
+ # }
9
+ # }
10
+ # }
11
+
12
+ class Mutations::ValidateToken < GraphQL::Schema::Mutation
13
+ field :valid, Boolean, null: false
14
+ field :user, Types::User, null: true
15
+
16
+ def resolve()
17
+ user = context[:current_user]
18
+
19
+ {
20
+ valid: user.present?,
21
+ user: user,
22
+ }
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Types::Error < Types::BaseObject
4
+ description 'Form error'
5
+
6
+ field :field, String, null: false do
7
+ description 'Field of the error'
8
+ end
9
+
10
+ field :message, String, null: false do
11
+ description 'Error message'
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # implements GraphQLAuth in in Types::MutationType to access auth mutations
2
+
3
+ module Types::GraphqlAuth
4
+ include GraphQL::Schema::Interface
5
+
6
+ field :sign_in, mutation: GraphQL::Auth.configuration.sign_in_mutation
7
+ field :sign_up, mutation: GraphQL::Auth.configuration.sign_up_mutation
8
+
9
+ field :forgot_password, mutation: GraphQL::Auth.configuration.forgot_password_mutation
10
+ field :reset_password, mutation: GraphQL::Auth.configuration.reset_password_mutation
11
+
12
+ field :update_account, mutation: GraphQL::Auth.configuration.update_account_mutation
13
+
14
+ field :validate_token, mutation: GraphQL::Auth.configuration.validate_token_mutation
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Types::User < Types::BaseObject
4
+ description 'Data of a user'
5
+
6
+ field :email, String, null: false do
7
+ description 'Email address of the user'
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ # include this helper in GraphqlController to use context method so that current_user will be available
2
+ #
3
+ # ::GraphqlSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
4
+
5
+ module Graphql
6
+ module AuthHelper
7
+ def context
8
+ {
9
+ current_user: current_user,
10
+ response: response,
11
+ }
12
+ end
13
+
14
+ def current_user
15
+ return if request.headers['Authorization'].nil?
16
+
17
+ decrypted_token = GraphQL::Auth::JwtManager.decode(request.headers['Authorization'])
18
+
19
+ user_id = decrypted_token['user']
20
+ user = User.find_by id: user_id # TODO use uuid
21
+
22
+ # update token if user is found with token
23
+ response.set_header 'Authorization', GraphQL::Auth::JwtManager.issue({ user: user.id }) if user.present?
24
+
25
+ user
26
+ rescue JWT::ExpiredSignature
27
+ nil
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,8 @@
1
+ <p>Hello <%= @resource.email %>!</p>
2
+
3
+ <p>Someone has requested a link to change your password. You can do this through the link below.</p>
4
+
5
+ <p><%= link_to 'Change my password', GraphQL::Auth::ResetPassword.url(@token) %></p>
6
+
7
+ <p>If you didn't request this, please ignore this email.</p>
8
+ <p>Your password won't change until you access the link above and create a new one.</p>
@@ -0,0 +1,11 @@
1
+ module GraphqlAuth
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def copy_configuration
7
+ template 'initializer.rb', 'config/initializers/graphql_auth.rb'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ GraphQL::Auth.configure do |config|
2
+ # config.token_lifespan = 4.hours
3
+ # config.jwt_secret_key = ENV['JWT_SECRET_KEY']
4
+ # config.app_url = ENV['APP_URL']
5
+
6
+ # config.sign_in_mutation = ::Mutations::SignIn
7
+ # config.sign_up_mutation = ::Mutations::SignUp
8
+
9
+ # config.forgot_password_mutation = ::Mutations::ForgotPassword
10
+ # config.reset_password_mutation = ::Mutations::ResetPassword
11
+
12
+ # config.update_account_mutation = ::Mutations::UpdateAccount
13
+
14
+ # config.validate_token_mutation = ::Mutations::ValidateToken
15
+ end
@@ -0,0 +1,19 @@
1
+ require 'graphql-auth/configuration'
2
+ require 'graphql-auth/engine'
3
+ require 'graphql-auth/reset_password'
4
+ require 'graphql-auth/jwt_manager'
5
+
6
+ module GraphQL
7
+ module Auth
8
+ class << self
9
+ attr_accessor :configuration
10
+ end
11
+
12
+ def self.configure
13
+ @configuration ||= Configuration.new
14
+ yield(configuration)
15
+ end
16
+ end
17
+ end
18
+
19
+
@@ -0,0 +1,31 @@
1
+ module GraphQL
2
+ module Auth
3
+ class Configuration
4
+ attr_accessor :token_lifespan,
5
+ :jwt_secret_key,
6
+ :app_url,
7
+ :sign_in_mutation,
8
+ :sign_up_mutation,
9
+ :forgot_password_mutation,
10
+ :reset_password_mutation,
11
+ :update_account_mutation,
12
+ :validate_token_mutation
13
+
14
+ def initialize
15
+ @token_lifespan = 4.hours
16
+ @jwt_secret_key = ENV['JWT_SECRET_KEY']
17
+ @app_url = ENV['APP_URL']
18
+
19
+ @sign_in_mutation = ::Mutations::SignIn
20
+ @sign_up_mutation = ::Mutations::SignUp
21
+
22
+ @forgot_password_mutation = ::Mutations::ForgotPassword
23
+ @reset_password_mutation = ::Mutations::ResetPassword
24
+
25
+ @update_account_mutation = ::Mutations::UpdateAccount
26
+
27
+ @validate_token_mutation = ::Mutations::ValidateToken
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module GraphQL
2
+ module Auth
3
+ class Engine < ::Rails::Engine
4
+ config.autoload_paths += Dir["#{config.root}/app/**/"]
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,48 @@
1
+ require 'jwt'
2
+ require 'graphql-auth'
3
+
4
+ module GraphQL
5
+ module Auth
6
+ class JwtManager
7
+ ALGORITHM = 'HS256'
8
+ TYPE = 'Bearer'
9
+
10
+ class << self
11
+ def issue(payload)
12
+ token = JWT.encode payload.merge(expiration),
13
+ auth_secret,
14
+ ALGORITHM
15
+ set_type token
16
+ end
17
+
18
+ def decode(token)
19
+ token = extract_token token
20
+ decrypted_token = JWT.decode token,
21
+ auth_secret,
22
+ true,
23
+ { algorithm: ALGORITHM }
24
+ decrypted_token.first
25
+ end
26
+
27
+ private
28
+
29
+ def auth_secret
30
+ GraphQL::Auth.configuration.jwt_secret_key
31
+ end
32
+
33
+ def set_type(token)
34
+ "#{TYPE} #{token}"
35
+ end
36
+
37
+ def extract_token(token)
38
+ token.gsub "#{TYPE} ", ''
39
+ end
40
+
41
+ def expiration
42
+ exp = Time.now.to_i + GraphQL::Auth.configuration.token_lifespan
43
+ { exp: exp }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,14 @@
1
+ require 'jwt'
2
+
3
+ module GraphQL
4
+ module Auth
5
+ class ResetPassword
6
+ class << self
7
+ def url(token)
8
+ url = I18n.locale === :fr ? 'nouveau-mot-de-passe' : 'new-password'
9
+ "#{GraphQL::Auth.configuration.app_url}/#{I18n.locale}/#{url}/#{token}"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql-auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Guillaume Ferland
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-10-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: graphql
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.8.5
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.8.5
55
+ - !ruby/object:Gem::Dependency
56
+ name: devise
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 4.4.3
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 4.4.3
69
+ - !ruby/object:Gem::Dependency
70
+ name: jwt
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.5.6
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.5.6
83
+ description: GraphQL + JWT + Devise
84
+ email:
85
+ - ferland182@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - Gemfile
91
+ - README.md
92
+ - app/graphql/mutations/forgot_password.rb
93
+ - app/graphql/mutations/reset_password.rb
94
+ - app/graphql/mutations/sign_in.rb
95
+ - app/graphql/mutations/sign_up.rb
96
+ - app/graphql/mutations/update_account.rb
97
+ - app/graphql/mutations/validate_token.rb
98
+ - app/graphql/types/error.rb
99
+ - app/graphql/types/graphql_auth.rb
100
+ - app/graphql/types/user.rb
101
+ - app/helpers/graphql/auth_helper.rb
102
+ - app/views/devise/mailer/reset_password_instructions.html.erb
103
+ - lib/generators/graphql_auth/install_generator.rb
104
+ - lib/generators/graphql_auth/templates/initializer.rb
105
+ - lib/graphql-auth.rb
106
+ - lib/graphql-auth/configuration.rb
107
+ - lib/graphql-auth/engine.rb
108
+ - lib/graphql-auth/jwt_manager.rb
109
+ - lib/graphql-auth/reset_password.rb
110
+ homepage: https://github.com/o2web/graphql-auth
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - app
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubyforge_project:
131
+ rubygems_version: 2.6.13
132
+ signing_key:
133
+ specification_version: 4
134
+ summary: GraphQL + JWT + Devise
135
+ test_files: []