Users & Auth
This commit is contained in:
23
app/commands/authenticate_user.rb
Normal file
23
app/commands/authenticate_user.rb
Normal file
@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AuthenticateUser < Imperator::Command
|
||||
include ActiveModel::Validations
|
||||
|
||||
string :email
|
||||
string :password
|
||||
|
||||
validates :email, presence: true
|
||||
validates :password, presence: true
|
||||
|
||||
def action
|
||||
JsonWebToken.encode(user_id: user.id) if user
|
||||
end
|
||||
|
||||
def user
|
||||
user = @user ||= User.find_by(email: @email)
|
||||
return user if user&.authenticate(@password)
|
||||
|
||||
errors.add :user_authentication, 'invalid credentials'
|
||||
nil
|
||||
end
|
||||
end
|
37
app/commands/authorize_request.rb
Normal file
37
app/commands/authorize_request.rb
Normal file
@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AuthorizeRequest < Imperator::Command
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_reader :headers
|
||||
|
||||
def initialize(headers)
|
||||
@headers = headers
|
||||
end
|
||||
|
||||
def action
|
||||
user
|
||||
end
|
||||
|
||||
def valid?
|
||||
headers["Authorization"].present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user
|
||||
@user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
|
||||
@user || errors.add(:token, 'Invalid token') && nil
|
||||
end
|
||||
|
||||
def decoded_auth_token
|
||||
@decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
|
||||
end
|
||||
|
||||
def http_auth_header
|
||||
return headers["Authorization"].split(' ').last if valid?
|
||||
|
||||
errors.add(:token, "Missing token")
|
||||
nil
|
||||
end
|
||||
end
|
@ -1,4 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationController < ActionController::API
|
||||
include Pundit
|
||||
|
||||
before_action :authenticate_request
|
||||
after_action :verify_authorized, except: :index
|
||||
after_action :verify_policy_scoped, only: :index
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
||||
|
||||
def index; end
|
||||
|
||||
private
|
||||
|
||||
def current_user
|
||||
@current_user ||= authenticate_request
|
||||
end
|
||||
|
||||
def authenticate_request
|
||||
return nil if request.authorization.blank?
|
||||
|
||||
@authenticate_request ||= AuthorizeRequest.new(request.headers).perform
|
||||
end
|
||||
|
||||
def user_not_authorized
|
||||
render \
|
||||
json: { authorization: ["You are not authorized to perform this action."] },
|
||||
status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
23
app/controllers/v1/authentication_controller.rb
Normal file
23
app/controllers/v1/authentication_controller.rb
Normal file
@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module V1
|
||||
class AuthenticationController < ApplicationController
|
||||
skip_after_action :verify_authorized
|
||||
skip_after_action :verify_policy_scoped
|
||||
|
||||
def authenticate
|
||||
command = AuthenticateUser.new(auth_params)
|
||||
@token = command.perform
|
||||
@user = command.user
|
||||
render "v1/authentication/authenticate" and return unless @token.nil?
|
||||
|
||||
render json: command.errors, status: :unauthorized
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def auth_params
|
||||
params.permit(:email, :password)
|
||||
end
|
||||
end
|
||||
end
|
47
app/controllers/v1/users_controller.rb
Normal file
47
app/controllers/v1/users_controller.rb
Normal file
@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module V1
|
||||
class UsersController < ApplicationController
|
||||
before_action :set_user, only: %i[show update destroy]
|
||||
|
||||
def index
|
||||
@users = policy_scope User.all
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@user = User.new(user_params)
|
||||
authorize @user
|
||||
|
||||
if @user.save
|
||||
render :show, status: :created, location: v1_users_url(@user)
|
||||
else
|
||||
render json: @user.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @user.update(user_params)
|
||||
render :show, status: :ok, location: v1_users_url(@user)
|
||||
else
|
||||
render json: @user.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user.destroy
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = User.find(params[:id])
|
||||
authorize @user
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(policy(User).permitted_attributes)
|
||||
end
|
||||
end
|
||||
end
|
15
app/libs/json_web_token.rb
Normal file
15
app/libs/json_web_token.rb
Normal file
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class JsonWebToken
|
||||
class << self
|
||||
def encode(payload, exp = 24.hours.from_now)
|
||||
payload[:exp] = exp.to_i
|
||||
JWT.encode(payload, ENV['jwt'])
|
||||
end
|
||||
|
||||
def decode(token)
|
||||
body = JWT.decode(token, ENV['jwt'])[0]
|
||||
HashWithIndifferentAccess.new body
|
||||
end
|
||||
end
|
||||
end
|
40
app/models/user.rb
Normal file
40
app/models/user.rb
Normal file
@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: users
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# display_name :string not null
|
||||
# email :string not null
|
||||
# password_digest :string not null
|
||||
# role :integer default("author"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_users_on_email (email)
|
||||
#
|
||||
|
||||
class User < ApplicationRecord
|
||||
has_secure_password
|
||||
|
||||
validates :display_name, presence: true
|
||||
validates :email, presence: true, email_format: true, uniqueness: true
|
||||
validates :password_confirmation, presence: true, if: ->(m) { m.password.present? }
|
||||
validates :role, presence: true
|
||||
|
||||
enum role: {
|
||||
author: 0,
|
||||
admin: 2
|
||||
}
|
||||
|
||||
def acts_as_admin?
|
||||
admin?
|
||||
end
|
||||
|
||||
def acts_as_author?
|
||||
admin? || author?
|
||||
end
|
||||
end
|
51
app/policies/application_policy.rb
Normal file
51
app/policies/application_policy.rb
Normal file
@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationPolicy
|
||||
attr_reader :user, :record
|
||||
|
||||
def initialize(user, record)
|
||||
@user = user
|
||||
@record = record
|
||||
end
|
||||
|
||||
def index?
|
||||
false
|
||||
end
|
||||
|
||||
def show?
|
||||
false
|
||||
end
|
||||
|
||||
def create?
|
||||
false
|
||||
end
|
||||
|
||||
def new?
|
||||
create?
|
||||
end
|
||||
|
||||
def update?
|
||||
false
|
||||
end
|
||||
|
||||
def edit?
|
||||
update?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
false
|
||||
end
|
||||
|
||||
class Scope
|
||||
attr_reader :user, :scope
|
||||
|
||||
def initialize(user, scope)
|
||||
@user = user
|
||||
@scope = scope
|
||||
end
|
||||
|
||||
def resolve
|
||||
scope.all
|
||||
end
|
||||
end
|
||||
end
|
52
app/policies/user_policy.rb
Normal file
52
app/policies/user_policy.rb
Normal file
@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserPolicy < ApplicationPolicy
|
||||
def show?
|
||||
raise Pundit::NotAuthorizedError if user.nil?
|
||||
return true if user&.acts_as_admin?
|
||||
|
||||
user == record
|
||||
end
|
||||
|
||||
def update?
|
||||
raise Pundit::NotAuthorizedError if user.nil?
|
||||
|
||||
show?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
raise Pundit::NotAuthorizedError if user.nil?
|
||||
|
||||
user&.acts_as_admin?
|
||||
end
|
||||
|
||||
def create?
|
||||
raise Pundit::NotAuthorizedError if user.nil?
|
||||
|
||||
user&.acts_as_admin?
|
||||
end
|
||||
|
||||
def permitted_attributes
|
||||
return base_attributes + %i[role] if user&.acts_as_admin?
|
||||
|
||||
base_attributes
|
||||
end
|
||||
|
||||
def base_attributes
|
||||
%i[
|
||||
display_name
|
||||
email
|
||||
password
|
||||
password_confirmation
|
||||
]
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
raise Pundit::NotAuthorizedError if user.nil?
|
||||
return scope if user.acts_as_admin?
|
||||
|
||||
scope.where(id: user.id)
|
||||
end
|
||||
end
|
||||
end
|
4
app/views/v1/authentication/authenticate.json.jbuilder
Normal file
4
app/views/v1/authentication/authenticate.json.jbuilder
Normal file
@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.auth_token @token
|
||||
json.user @user, partial: "v1/users/user", as: :user
|
9
app/views/v1/users/_user.json.jbuilder
Normal file
9
app/views/v1/users/_user.json.jbuilder
Normal file
@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! user,
|
||||
:id,
|
||||
:display_name,
|
||||
:email,
|
||||
:role
|
||||
|
||||
json.url v1_user_url(user, format: :json)
|
3
app/views/v1/users/index.json.jbuilder
Normal file
3
app/views/v1/users/index.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array! @users, partial: 'v1/users/user', as: :user
|
3
app/views/v1/users/show.json.jbuilder
Normal file
3
app/views/v1/users/show.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! "v1/users/user", user: @user
|
Reference in New Issue
Block a user