Permalink
Browse files

GraphQL server with cookie-based user registration, login and logout.

  • Loading branch information...
dblock committed Jul 7, 2018
1 parent 8050ab4 commit 52a24924b5b0bfae41fa33ef8141099cbf66c351
1 .rspec
@@ -1 +1,2 @@
--require spec_helper
--format documentation
@@ -1,11 +1,21 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2018-07-05 20:44:56 -0400 using RuboCop version 0.57.2.
# on 2018-07-07 15:37:08 -0400 using RuboCop version 0.57.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

# Offense count: 3
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: line_count_dependent, lambda, literal
Style/Lambda:
Exclude:
- 'app/graphql/mutations/create_user_mutation.rb'
- 'app/graphql/mutations/login_mutation.rb'
- 'app/graphql/mutations/logout_mutation.rb'

# Offense count: 2
Style/MixinUsage:
Exclude:
@@ -2,5 +2,8 @@ language: ruby

cache: bundler

services:
- mongodb

rvm:
- 2.5.1
11 Gemfile
@@ -3,14 +3,21 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.5.1'

gem 'bcrypt'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'graphql'
gem 'graphql-errors'
gem 'mongoid'
gem 'puma', '~> 3.11'
gem 'rails', '~> 5.2.0'

gem 'rack-cors'
gem 'rails', '~> 5.2.0'
gem 'warden'

group :development, :test do
gem 'database_cleaner'
gem 'fabrication'
gem 'faker'
gem 'graphlient'
gem 'rspec-rails'
gem 'rubocop'
end
@@ -44,20 +44,39 @@ GEM
tzinfo (~> 1.1)
arel (9.0.0)
ast (2.4.0)
bcrypt (3.1.12)
bootsnap (1.3.0)
msgpack (~> 1.0)
bson (4.3.0)
builder (3.2.3)
concurrent-ruby (1.0.5)
crass (1.0.4)
database_cleaner (1.7.0)
diff-lcs (1.3)
erubi (1.7.1)
fabrication (2.20.1)
faker (1.8.7)
i18n (>= 0.7)
faraday (0.15.2)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.12.2)
faraday (>= 0.7.4, < 1.0)
ffi (1.9.25)
globalid (0.4.1)
activesupport (>= 4.2.0)
graphlient (0.3.2)
faraday
faraday_middleware
graphql-client
graphql (1.8.4)
graphql-client (0.12.3)
activesupport (>= 3.0, < 6.0)
graphql (~> 1.6)
graphql-errors (0.2.0)
graphql (>= 1.6.0, < 2)
i18n (1.0.1)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.1-x86_64-darwin-17)
jaro_winkler (1.5.1)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
@@ -80,6 +99,7 @@ GEM
activemodel (>= 5.1, < 6.0.0)
mongo (>= 2.5.1, < 3.0.0)
msgpack (1.2.4)
multipart-post (2.0.0)
nio4r (2.3.1)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
@@ -165,6 +185,8 @@ GEM
tzinfo (1.2.5)
thread_safe (~> 0.1)
unicode-display_width (1.4.0)
warden (1.2.7)
rack (>= 1.0)
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
@@ -173,7 +195,14 @@ PLATFORMS
ruby

DEPENDENCIES
bcrypt
bootsnap (>= 1.1.0)
database_cleaner
fabrication
faker
graphlient
graphql
graphql-errors
listen (>= 3.0.5, < 3.2)
mongoid
puma (~> 3.11)
@@ -183,6 +212,7 @@ DEPENDENCIES
rubocop
spring
spring-watcher-listen (~> 2.0.0)
warden

RUBY VERSION
ruby 2.5.1p57
@@ -0,0 +1,40 @@
class GraphqlController < ApplicationController
def execute
result = Schema.execute(
query,
variables: variables,
context: context,
operation_name: operation_name
)
render json: result
end

private

def query
params[:query]
end

def operation_name
params[:operationName]
end

def context
{
warden: warden,
current_user: current_user
}
end

def variables
params[:variables] || {}
end

def current_user
warden.user
end

def warden
request.env['warden']
end
end
@@ -0,0 +1,20 @@
Mutations::CreateUserMutation = GraphQL::Relay::Mutation.define do
name 'createUser'

input_field :email, !types.String
input_field :password, !types.String
input_field :name, !types.String

return_field :user, Types::UserType

resolve ->(_object, inputs, ctx) {
user = User.create!(
email: inputs[:email],
name: inputs[:name],
password: inputs[:password]
)

ctx[:warden].set_user(user)
{ user: user }
}
end
@@ -0,0 +1,19 @@
Mutations::LoginMutation = GraphQL::Relay::Mutation.define do
name 'login'

input_field :email, !types.String
input_field :password, !types.String

return_field :user, Types::UserType

resolve ->(_object, inputs, ctx) {
user = User.where(email: inputs[:email]).first

if user && user.authenticate(inputs[:password])
ctx[:warden].set_user(user)
{ user: user }
else
GraphQL::ExecutionError.new('Incorrect email or password.')
end
}
end
@@ -0,0 +1,14 @@
Mutations::LogoutMutation = GraphQL::Relay::Mutation.define do
name 'logout'

return_field :user, Types::UserType

resolve ->(_object, _inputs, ctx) {
if ctx[:current_user]
ctx[:warden].logout
{ user: ctx[:current_user] }
else
GraphQL::ExecutionError.new('Not logged in.')
end
}
end
@@ -0,0 +1,19 @@
Schema = GraphQL::Schema.define do
query Types::QueryType
mutation Types::MutationType
end

GraphQL::Errors.configure(Schema) do
rescue_from Mongoid::Errors::DocumentNotFound do
nil
end

rescue_from Mongoid::Errors::Validations do |e|
error_messages = e.record.errors.full_messages.join("\n")
GraphQL::ExecutionError.new "Validation failed: #{error_messages}"
end

rescue_from StandardError do |e|
GraphQL::ExecutionError.new e.message
end
end
@@ -0,0 +1,6 @@
Types::DateTimeType = GraphQL::ScalarType.define do
name 'DateTime'

coerce_input ->(value, _ctx) { Time.zone.parse(value) }
coerce_result ->(value, _ctx) { value.utc.iso8601 }
end
@@ -0,0 +1,7 @@
Types::MutationType = GraphQL::ObjectType.define do
name 'Mutation'

field :createUser, field: Mutations::CreateUserMutation.field
field :login, field: Mutations::LoginMutation.field
field :logout, field: Mutations::LogoutMutation.field
end
@@ -0,0 +1,4 @@
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
description 'The query root of this schema. See available queries.'
end
@@ -0,0 +1,9 @@
Types::UserType = GraphQL::ObjectType.define do
name 'User'
description 'A user.'

field :id, types.ID, 'User ID.'
field :name, types.String, 'User full name.'
field :email, types.String, 'User email.'
field :created_at, Types::DateTimeType, 'Account creation date.'
end
@@ -0,0 +1,31 @@
class User
include Mongoid::Document
include Mongoid::Timestamps

field :name, type: String

field :email, type: String
validates_presence_of :email
index({ email: 1 }, unique: true)

attr_accessor :password

field :password_digest, type: String
validates_presence_of :password_digest

validates_presence_of :email, message: 'Email address is required.'
validates_uniqueness_of :email, message: 'Email address already in use.'
validates_format_of :email, with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i, message: 'Please enter a valid email address.'

before_validation :encrypt_password

def authenticate(password)
return self if BCrypt::Password.new(password_digest) == password
end

protected

def encrypt_password
self.password_digest = BCrypt::Password.create(password) if password
end
end
@@ -1,35 +1,22 @@
require_relative 'boot'

require 'rails'
# Pick the frameworks you want:
require 'active_model/railtie'
require 'active_job/railtie'
# require "active_record/railtie"
# require "active_storage/engine"
require 'action_controller/railtie'
require 'action_mailer/railtie'
require 'action_view/railtie'
require 'action_cable/engine'
# require "sprockets/railtie"
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Server
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2

# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.

# Only loads a smaller set of middleware suitable for API only apps.
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
config.autoload_paths << Rails.root.join('app/graphql')
config.autoload_paths << Rails.root.join('app/graphql/types')
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore, key: '_namespace_key'
end
end
@@ -32,6 +32,8 @@

config.action_mailer.perform_caching = false

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log

@@ -5,12 +5,12 @@

# Read more: https://github.com/cyu/rack-cors

# Rails.application.config.middleware.insert_before 0, Rack::Cors do
# allow do
# origins 'example.com'
#
# resource '*',
# headers: :any,
# methods: [:get, :post, :put, :patch, :delete, :options, :head]
# end
# end
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'

resource '*',
headers: :any,
methods: %i[get post put patch delete options head]
end
end
@@ -0,0 +1,3 @@
Rails.application.config.middleware.insert_after Rack::ETag, Warden::Manager do |manager|
manager.failure_app = GraphqlController
end
@@ -1,3 +1,3 @@
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
post '/graphql', to: 'graphql#execute'
end
Oops, something went wrong.

0 comments on commit 52a2492

Please sign in to comment.