Skip to content

Commit

Permalink
Auth: User model and loginUser mutation
Browse files Browse the repository at this point in the history
  • Loading branch information
ztratify committed Dec 1, 2020
1 parent 2e6a812 commit d321a7d
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 5 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ gem 'bootsnap', '>= 1.4.2', require: false
# Graphql rails
gem 'graphql', '1.9.17'

# Ensure secure passwords
gem 'bcrypt', '~> 3.1.13'

group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ GEM
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
bcrypt (3.1.16)
bindex (0.8.1)
bootsnap (1.5.1)
msgpack (~> 1.0)
Expand Down Expand Up @@ -170,6 +171,7 @@ PLATFORMS
ruby

DEPENDENCIES
bcrypt (~> 3.1.13)
bootsnap (>= 1.4.2)
byebug
graphiql-rails (= 1.7.0)
Expand Down
20 changes: 17 additions & 3 deletions app/controllers/graphql_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ class GraphqlController < ApplicationController
# If accessing from outside this domain, nullify the session
# This allows for outside API access while preventing CSRF attacks,
# but you'll have to authenticate your user separately
# protect_from_forgery with: :null_session
protect_from_forgery with: :null_session

def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
# Query context goes here, for example:
# current_user: current_user,
# we need to provide session and current user to all requests
session: session,
current_user: current_user
}
result = RailsHowtographqlSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
Expand All @@ -21,6 +22,19 @@ def execute

private

# gets current user from token stored in the session
def current_user
# if we want to change the sign-in strategy, this is the place to do it
return unless session[:token]

crypt = ActiveSupport::MessageEncryptor.new(Rails.application.credentials.secret_key_base.byteslice(0..31))
token = crypt.decrypt_and_verify session[:token]
user_id = token.gsub('user-id:', '').to_i
User.find user_id
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
end

# Handle form data, JSON body, or a blank value
def ensure_hash(ambiguous_param)
case ambiguous_param
Expand Down
22 changes: 22 additions & 0 deletions app/graphql/mutations/create_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module Mutations
class CreateUser < BaseMutation
# often we will need input types for specific mutation
# in those cases we can define those input types in the mutation class itself
class AuthProviderSignupData < Types::BaseInputObject
argument :credentials, Types::AuthProviderCredentialsInput, required: false
end

argument :name, String, required: true
argument :auth_provider, AuthProviderSignupData, required: false

type Types::UserType

def resolve(name: nil, auth_provider: nil)
User.create!(
name: name,
email: auth_provider&.[](:credentials)&.[](:email),
password: auth_provider&.[](:credentials)&.[](:password)
)
end
end
end
31 changes: 31 additions & 0 deletions app/graphql/mutations/login_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module Mutations
class LoginUser < BaseMutation
null true

argument :credentials, Types::AuthProviderCredentialsInput, required: false

field :token, String, null: true
field :user, Types::UserType, null: true

def resolve(credentials: nil)
# basic validation
return unless credentials

user = User.find_by email: credentials[:email]

# ensures we have the correct user
return unless user
return unless user.authenticate(credentials[:password])

# use Ruby on Rails - ActiveSupport::MessageEncryptor, to build a token
crypt = ActiveSupport::MessageEncryptor.new(Rails.application.credentials.secret_key_base.byteslice(0..31))
token = crypt.encrypt_and_sign("user-id:#{ user.id }")

# WARNING: we're storing decrypted tokens on each request!
# Be sure to check out a more secure token method when building a real-world application, such as JWT.
context[:session][:token] = token

{ user: user, token: token }
end
end
end
9 changes: 9 additions & 0 deletions app/graphql/types/auth_provider_credentials_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Types
class AuthProviderCredentialsInput < BaseInputObject
# the name is usually inferred by class name but can be overwritten
graphql_name 'AUTH_PROVIDER_CREDENTIALS'

argument :email, String, required: true
argument :password, String, required: true
end
end
4 changes: 3 additions & 1 deletion app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Types
class MutationType < BaseObject
field :create_link, mutation: Mutations::CreateLink
field :create_user, mutation: Mutations::CreateUser
field :login_user, mutation: Mutations::LoginUser
end
end
end
7 changes: 7 additions & 0 deletions app/graphql/types/user_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: false
end
end
6 changes: 6 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class User < ApplicationRecord
has_secure_password

validates :name, presence: true
validates :email, presence: true, uniqueness: true
end
11 changes: 11 additions & 0 deletions db/migrate/20201130014852_create_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password_digest

t.timestamps
end
end
end
10 changes: 9 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2020_11_30_011635) do
ActiveRecord::Schema.define(version: 2020_11_30_014852) do

create_table "links", force: :cascade do |t|
t.string "url"
Expand All @@ -19,4 +19,12 @@
t.datetime "updated_at", precision: 6, null: false
end

create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.string "password_digest"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end

end
11 changes: 11 additions & 0 deletions test/fixtures/users.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
name: MyString
email: MyString
password_digest: MyString

two:
name: MyString
email: MyString
password_digest: MyString
23 changes: 23 additions & 0 deletions test/graphql/mutations/create_user_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'test_helper'

class Mutations::CreateUserTest < ActiveSupport::TestCase
def perform(args = {})
Mutations::CreateUser.new(object: nil, field: nil, context: {}).resolve(args)
end

test 'create new user' do
user = perform(
name: 'Test User',
auth_provider: {
credentials: {
email: '[email protected]',
password: '[omitted]'
}
}
)

assert user.persisted?
assert_equal user.name, 'Test User'
assert_equal user.email, '[email protected]'
end
end
43 changes: 43 additions & 0 deletions test/graphql/mutations/login_user_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require 'test_helper'

class Mutations::SignInUserTest < ActiveSupport::TestCase
def perform(args = {})
Mutations::SignInUser.new(object: nil, field: nil, context: { session: {} }).resolve(args)
end

def create_user
User.create!(
name: 'Test User',
email: '[email protected]',
password: '[omitted]',
)
end

test 'success' do
user = create_user

result = perform(
credentials: {
email: user.email,
password: user.password
}
)

assert result[:token].present?
assert_equal result[:user], user
end

test 'failure because no credentials' do
assert_nil perform
end

test 'failure because wrong email' do
create_user
assert_nil perform(credentials: { email: 'wrong' })
end

test 'failure because wrong password' do
user = create_user
assert_nil perform(credentials: { email: user.email, password: 'wrong' })
end
end
7 changes: 7 additions & 0 deletions test/models/user_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require 'test_helper'

class UserTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

0 comments on commit d321a7d

Please sign in to comment.