Users & Auth
This commit is contained in:
parent
904a071fc0
commit
8a7b3d8ae0
1
Gemfile
1
Gemfile
@ -10,6 +10,7 @@ gem 'rails', '~> 5.2.1'
|
|||||||
|
|
||||||
gem 'bcrypt', '~> 3.1.7'
|
gem 'bcrypt', '~> 3.1.7'
|
||||||
gem 'bootsnap', '>= 1.1.0', require: false
|
gem 'bootsnap', '>= 1.1.0', require: false
|
||||||
|
gem 'imperator', github: 'karmajunkie/imperator'
|
||||||
gem 'jbuilder', '~> 2.5'
|
gem 'jbuilder', '~> 2.5'
|
||||||
gem 'json', '~> 2.1'
|
gem 'json', '~> 2.1'
|
||||||
gem 'jwt', '2.1.0'
|
gem 'jwt', '2.1.0'
|
||||||
|
23
Gemfile.lock
23
Gemfile.lock
@ -21,6 +21,13 @@ GIT
|
|||||||
shellany (~> 0.0)
|
shellany (~> 0.0)
|
||||||
thor (>= 0.18.1)
|
thor (>= 0.18.1)
|
||||||
|
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/karmajunkie/imperator.git
|
||||||
|
revision: 7256d702be8ac70706b2e8101e2326c50f0c6fb2
|
||||||
|
specs:
|
||||||
|
imperator (0.2.0)
|
||||||
|
virtus (~> 1.0.0)
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/kern/minitest-reporters.git
|
remote: https://github.com/kern/minitest-reporters.git
|
||||||
revision: d04c7e9b0504542f6084dc5a2060c774dc6cdda4
|
revision: d04c7e9b0504542f6084dc5a2060c774dc6cdda4
|
||||||
@ -82,6 +89,10 @@ GEM
|
|||||||
arel (9.0.0)
|
arel (9.0.0)
|
||||||
ast (2.4.0)
|
ast (2.4.0)
|
||||||
awesome_print (1.8.0)
|
awesome_print (1.8.0)
|
||||||
|
axiom-types (0.1.1)
|
||||||
|
descendants_tracker (~> 0.0.4)
|
||||||
|
ice_nine (~> 0.11.0)
|
||||||
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
bcrypt (3.1.12)
|
bcrypt (3.1.12)
|
||||||
binding_of_caller (0.8.0)
|
binding_of_caller (0.8.0)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
@ -92,10 +103,15 @@ GEM
|
|||||||
byebug (10.0.2)
|
byebug (10.0.2)
|
||||||
choice (0.2.0)
|
choice (0.2.0)
|
||||||
coderay (1.1.2)
|
coderay (1.1.2)
|
||||||
|
coercible (1.0.0)
|
||||||
|
descendants_tracker (~> 0.0.1)
|
||||||
concurrent-ruby (1.1.3)
|
concurrent-ruby (1.1.3)
|
||||||
crass (1.0.4)
|
crass (1.0.4)
|
||||||
debug_inspector (0.0.3)
|
debug_inspector (0.0.3)
|
||||||
|
descendants_tracker (0.0.4)
|
||||||
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
docile (1.3.1)
|
docile (1.3.1)
|
||||||
|
equalizer (0.0.11)
|
||||||
erubi (1.7.1)
|
erubi (1.7.1)
|
||||||
ffi (1.9.25)
|
ffi (1.9.25)
|
||||||
figaro (1.1.1)
|
figaro (1.1.1)
|
||||||
@ -122,6 +138,7 @@ GEM
|
|||||||
spring
|
spring
|
||||||
i18n (1.1.1)
|
i18n (1.1.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
ice_nine (0.11.2)
|
||||||
jaro_winkler (1.5.1)
|
jaro_winkler (1.5.1)
|
||||||
jbuilder (2.8.0)
|
jbuilder (2.8.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
@ -243,6 +260,11 @@ GEM
|
|||||||
unicode-display_width (1.4.0)
|
unicode-display_width (1.4.0)
|
||||||
validates_email_format_of (1.6.3)
|
validates_email_format_of (1.6.3)
|
||||||
i18n
|
i18n
|
||||||
|
virtus (1.0.5)
|
||||||
|
axiom-types (~> 0.1)
|
||||||
|
coercible (~> 1.0)
|
||||||
|
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||||
|
equalizer (~> 0.0, >= 0.0.9)
|
||||||
websocket-driver (0.7.0)
|
websocket-driver (0.7.0)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.3)
|
websocket-extensions (0.1.3)
|
||||||
@ -265,6 +287,7 @@ DEPENDENCIES
|
|||||||
guard-rubocop
|
guard-rubocop
|
||||||
guard-shell
|
guard-shell
|
||||||
guard-spring
|
guard-spring
|
||||||
|
imperator!
|
||||||
jbuilder (~> 2.5)
|
jbuilder (~> 2.5)
|
||||||
json (~> 2.1)
|
json (~> 2.1)
|
||||||
jwt (= 2.1.0)
|
jwt (= 2.1.0)
|
||||||
|
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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ApplicationController < ActionController::API
|
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
|
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
|
@ -10,5 +10,6 @@
|
|||||||
# stripe_api_key: sk_live_EeHnL644i6zo4Iyq4v1KdV9H
|
# stripe_api_key: sk_live_EeHnL644i6zo4Iyq4v1KdV9H
|
||||||
# stripe_publishable_key: pk_live_9lcthxpSIHbGwmdO941O1XVU
|
# stripe_publishable_key: pk_live_9lcthxpSIHbGwmdO941O1XVU
|
||||||
|
|
||||||
|
jwt: 98a6c2b7c9a1eb1da66595970e6f699a2e1fb339c2b759f443380ad86a95ba299bc03f5b38681b57bbc5e0bbedb3ee2d61e6a5e5b4f836ddfd22c27fa09b7fbb
|
||||||
aws_key: this-is-the-key
|
aws_key: this-is-the-key
|
||||||
aws_secret: this-is-the-secret
|
aws_secret: this-is-the-secret
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
|
concern :api_base do
|
||||||
|
post 'authenticate', to: 'authentication#authenticate', as: :authenticate
|
||||||
|
resources :users
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace :v1, defaults: { format: :json } do
|
||||||
|
concerns :api_base
|
||||||
|
end
|
||||||
|
|
||||||
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
|
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
|
||||||
end
|
end
|
||||||
|
15
db/migrate/20181110183741_create_users.rb
Normal file
15
db/migrate/20181110183741_create_users.rb
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateUsers < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :users do |t|
|
||||||
|
t.string :display_name, null: false
|
||||||
|
t.string :email, null: false
|
||||||
|
t.string :password_digest, null: false
|
||||||
|
t.integer :role, null: false, default: 0
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
add_index :users, :email
|
||||||
|
end
|
||||||
|
end
|
12
db/schema.rb
12
db/schema.rb
@ -10,6 +10,16 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 0) do
|
ActiveRecord::Schema.define(version: 2018_11_10_183741) do
|
||||||
|
|
||||||
|
create_table "users", force: :cascade do |t|
|
||||||
|
t.string "display_name", null: false
|
||||||
|
t.string "email", null: false
|
||||||
|
t.string "password_digest", null: false
|
||||||
|
t.integer "role", default: 0, null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["email"], name: "index_users_on_email"
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
19
test/controllers/v1/authentication_controller_test.rb
Normal file
19
test/controllers/v1/authentication_controller_test.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class AuthenticationControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
@user = users(:admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should return token" do
|
||||||
|
post v1_authenticate_url, params: { email: @user.email, password: 'password' }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should fail auth" do
|
||||||
|
post v1_authenticate_url, params: { email: @user.email, password: 'BAD PASSWORD' }
|
||||||
|
assert_response :unauthorized
|
||||||
|
end
|
||||||
|
end
|
68
test/controllers/v1/users_controller_test.rb
Normal file
68
test/controllers/v1/users_controller_test.rb
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class UsersControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
test "admin can list users" do
|
||||||
|
get v1_users_url, headers: auth_headers(users(:admin))
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can create user" do
|
||||||
|
assert_difference('User.count') do
|
||||||
|
post v1_users_url, params: { user: {
|
||||||
|
display_name: 'some user',
|
||||||
|
email: 'new.user@mailinator.com',
|
||||||
|
password: 'password',
|
||||||
|
password_confirmation: 'password'
|
||||||
|
} }, headers: auth_headers(users(:admin))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :created
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can view users" do
|
||||||
|
get v1_user_url(users(:admin)), headers: auth_headers(users(:admin))
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can update user" do
|
||||||
|
patch v1_user_url(users(:admin)), params: { user: {
|
||||||
|
display_name: 'I am admin'
|
||||||
|
} }, headers: auth_headers(users(:admin))
|
||||||
|
|
||||||
|
assert_response :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can destroy user" do
|
||||||
|
assert_difference('User.count', -1) do
|
||||||
|
delete v1_user_url(users(:admin)), headers: auth_headers(users(:admin))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
test "author can view herself" do
|
||||||
|
get v1_user_url(users(:author)), headers: auth_headers(users(:author))
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "author can update herself" do
|
||||||
|
patch v1_user_url(users(:author)), params: { user: {
|
||||||
|
display_name: 'I am author!'
|
||||||
|
} }, headers: auth_headers(users(:author))
|
||||||
|
|
||||||
|
assert_response :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sally CANNOT update phil" do
|
||||||
|
patch v1_user_url(users(:author)), params: { user: {
|
||||||
|
display_name: 'I am author!'
|
||||||
|
} }, headers: auth_headers(users(:sally))
|
||||||
|
|
||||||
|
assert_response :unauthorized
|
||||||
|
end
|
||||||
|
end
|
42
test/fixtures/users.yml
vendored
Normal file
42
test/fixtures/users.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# == 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)
|
||||||
|
#
|
||||||
|
|
||||||
|
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
|
admin:
|
||||||
|
display_name: Awesome Admin
|
||||||
|
email: awesome.admin@mailinator.com
|
||||||
|
password_digest: <%= BCrypt::Password.create("password", cost: 4) %>
|
||||||
|
role: admin
|
||||||
|
|
||||||
|
author:
|
||||||
|
display_name: lePhil
|
||||||
|
email: lephil@mailinator.com
|
||||||
|
password_digest: <%= BCrypt::Password.create("password", cost: 4) %>
|
||||||
|
role: author
|
||||||
|
|
||||||
|
sally:
|
||||||
|
display_name: Sally String
|
||||||
|
email: sally.string@mailinator.com
|
||||||
|
password_digest: <%= BCrypt::Password.create("password", cost: 4) %>
|
||||||
|
role: author
|
||||||
|
|
||||||
|
michelle:
|
||||||
|
display_name: Mighty Michelle
|
||||||
|
email: mighty.michelle@mailinator.com
|
||||||
|
password_digest: <%= BCrypt::Password.create("password", cost: 4) %>
|
||||||
|
role: author
|
16
test/models/user_test.rb
Normal file
16
test/models/user_test.rb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class UserTest < ActiveSupport::TestCase
|
||||||
|
test "higer roles can act as lower roles" do
|
||||||
|
assert users(:admin).acts_as_admin?
|
||||||
|
assert users(:admin).acts_as_author?
|
||||||
|
|
||||||
|
assert users(:author).acts_as_author?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "lower roles can NOT act as higher roles" do
|
||||||
|
assert_not users(:author).acts_as_admin?
|
||||||
|
end
|
||||||
|
end
|
18
test/policies/application_policy_test.rb
Normal file
18
test/policies/application_policy_test.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class ApplicationPolicyTest < PolicyAssertions::Test
|
||||||
|
# Verify default policies are most restrictive
|
||||||
|
|
||||||
|
test 'should not permit by default' do
|
||||||
|
admin = users(:admin)
|
||||||
|
assert_not ApplicationPolicy.new(admin, nil).show?
|
||||||
|
assert_not ApplicationPolicy.new(admin, nil).index?
|
||||||
|
assert_not ApplicationPolicy.new(admin, nil).create?
|
||||||
|
assert_not ApplicationPolicy.new(admin, nil).new?
|
||||||
|
assert_not ApplicationPolicy.new(admin, nil).update?
|
||||||
|
assert_not ApplicationPolicy.new(admin, nil).edit?
|
||||||
|
assert_not ApplicationPolicy.new(admin, nil).destroy?
|
||||||
|
end
|
||||||
|
end
|
94
test/policies/user_policy_test.rb
Normal file
94
test/policies/user_policy_test.rb
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class UserPolicyTest < PolicyAssertions::Test
|
||||||
|
test 'must authenticate for actions' do
|
||||||
|
assert_raise Pundit::NotAuthorizedError do
|
||||||
|
%w[create show update destroy].each do |action|
|
||||||
|
UserPolicy.new(nil, User.new).send("#{action}?")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'should allow admin to scope' do
|
||||||
|
scope = UserPolicy::Scope.new(users(:admin), User).resolve
|
||||||
|
assert_equal User.count, scope.count
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'non admins can only scope themselves' do
|
||||||
|
%i[author].each do |user|
|
||||||
|
scope = UserPolicy::Scope.new(users(user), User).resolve
|
||||||
|
assert_equal 1, scope.count, "Scope did not have 1 result for #{user}"
|
||||||
|
assert_equal users(user), scope.first, "Scope did not contain self for #{user}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'admins have role in permitted params' do
|
||||||
|
policy = UserPolicy.new users(:admin), nil
|
||||||
|
assert policy.permitted_attributes.include?(:role)
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'non-admins can not edit roles' do
|
||||||
|
%i[author].each do |user|
|
||||||
|
policy = UserPolicy.new users(user), nil
|
||||||
|
assert_not policy.permitted_attributes.include?(:role)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# create
|
||||||
|
test 'only admins can create' do
|
||||||
|
assert_permit users(:admin), User, :create?
|
||||||
|
|
||||||
|
%i[author].each do |user|
|
||||||
|
assert_not_permitted users(user), User, :create?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# delete
|
||||||
|
test 'only admins can destroy' do
|
||||||
|
assert_permit users(:admin), User, :destroy?
|
||||||
|
|
||||||
|
%i[author].each do |user|
|
||||||
|
assert_not_permitted users(user), User, :destroy?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# show
|
||||||
|
test 'admin can view any role' do
|
||||||
|
%i[admin author].each do |user|
|
||||||
|
assert_permit users(:admin), users(user), :show?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'non-admins can view themselves' do
|
||||||
|
%i[author].each do |user|
|
||||||
|
assert_permit users(user), users(user), :show?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'author roles can only view themselves' do
|
||||||
|
%i[admin sally michelle].each do |user|
|
||||||
|
assert_not_permitted users(:author), users(user), :show?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# updates
|
||||||
|
test 'admin can update any role' do
|
||||||
|
%i[admin author].each do |user|
|
||||||
|
assert_permit users(:admin), users(user), :update?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'non-admins can update themselves' do
|
||||||
|
%i[author].each do |user|
|
||||||
|
assert_permit users(user), users(user), :update?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'authors can not update other roles' do
|
||||||
|
%i[admin sally michelle].each do |user|
|
||||||
|
assert_not_permitted users(:author), users(user), :update?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -3,14 +3,8 @@
|
|||||||
An API for a micro blogging platform
|
An API for a micro blogging platform
|
||||||
|
|
||||||
Guests can only view published content and author list
|
Guests can only view published content and author list
|
||||||
Standard can edit their profile & blogs
|
Authors can edit their profile & blogs
|
||||||
Admins can view list of users and edit any blog
|
Admins can view the list of authors and edit any blog
|
||||||
|
|
||||||
users:
|
|
||||||
display_name
|
|
||||||
email
|
|
||||||
password_digest
|
|
||||||
roles: guest, standard, admin
|
|
||||||
|
|
||||||
blogs:
|
blogs:
|
||||||
title
|
title
|
||||||
@ -18,20 +12,25 @@ blogs:
|
|||||||
published_date
|
published_date
|
||||||
author_id
|
author_id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
### User Bios
|
### User Bios
|
||||||
|
|
||||||
As a user, I want to be able to author a bio that is
|
As an author, I want to be able to write a short bio that
|
||||||
displayed with each of my postings.
|
is displayed with each of my postings.
|
||||||
|
|
||||||
### Moderation
|
### Moderation
|
||||||
|
|
||||||
As an admin, I want to be able to put a blog it in a state
|
As an admin, I want to be able to put a blog it in a state
|
||||||
of moderation.
|
of moderation that also unpublishes it.
|
||||||
|
|
||||||
Once a blog post has this state, the author must edit and
|
Once a blog post has this state, the author must edit and
|
||||||
submit for another review before an admin will republish.
|
submit for another review before an admin will republish.
|
||||||
|
|
||||||
|
### Categories
|
||||||
|
|
||||||
|
As an author, I would like to be able to assign as category
|
||||||
|
to my posting from a known list of options.
|
||||||
|
|
||||||
|
As an admin, I can manage the list of categories available
|
||||||
|
to the authors.
|
||||||
|
Loading…
Reference in New Issue
Block a user