Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .allow_skipping_tests
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ models/concerns/by_organization_scope.rb
models/concerns/roles.rb
models/fund_request.rb
models/notification.rb
models/jwt_denylist.rb
notifications/base_notification.rb
notifications/delivery_methods/sms.rb
notifications/emancipation_checklist_reminder_notification.rb
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ gem "cssbundling-rails", "~> 1.1" # compiles css
gem "delayed_job_active_record"
gem "devise" # for authentication
gem "devise_invitable"
gem "devise-jwt"
gem "rack-cors"
gem "httparty" # for making HTTP network requests 🥳
gem "twilio-ruby" # twilio helper functions
gem "draper" # adds decorators for cleaner presentation logic
Expand Down
27 changes: 24 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise_invitable (2.0.8)
devise-jwt (0.10.0)
devise (~> 4.0)
warden-jwt_auth (~> 0.6)
devise_invitable (2.0.7)
actionmailer (>= 5.0)
devise (>= 4.6)
diff-lcs (1.5.0)
Expand All @@ -166,7 +169,16 @@ GEM
activesupport (>= 5.0)
request_store (>= 1.0)
ruby2_keywords
erb_lint (0.4.0)
dry-auto_inject (1.0.1)
dry-core (~> 1.0)
zeitwerk (~> 2.6)
dry-configurable (1.0.1)
dry-core (~> 1.0, < 2)
zeitwerk (~> 2.6)
dry-core (1.0.0)
concurrent-ruby (~> 1.0)
zeitwerk (~> 2.6)
erb_lint (0.3.1)
activesupport
better_html (>= 2.0.1)
parser (>= 2.7.1.4)
Expand Down Expand Up @@ -319,7 +331,9 @@ GEM
rack (2.2.7)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-test (2.1.0)
rack-cors (2.0.0)
rack (>= 2.0.0)
rack-test (2.0.2)
rack (>= 1.3)
rails (7.0.5)
actioncable (= 7.0.5)
Expand Down Expand Up @@ -452,6 +466,11 @@ GEM
method_source (~> 1.0)
warden (1.2.9)
rack (>= 2.0.9)
warden-jwt_auth (0.8.0)
dry-auto_inject (>= 0.8, < 2)
dry-configurable (>= 0.13, < 2)
jwt (~> 2.1)
warden (~> 1.2)
web-console (4.2.0)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
Expand Down Expand Up @@ -502,6 +521,7 @@ DEPENDENCIES
database_cleaner-active_record
delayed_job_active_record
devise
devise-jwt
devise_invitable
dotenv-rails
draper
Expand Down Expand Up @@ -529,6 +549,7 @@ DEPENDENCIES
puma (= 6.2.2)
pundit
rack-attack
rack-cors
rails (~> 7.0.5)
rails-controller-testing
rake
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/concerns/accessible.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def check_user
flash.clear
redirect_to(authenticated_all_casa_admin_root_path) and return
# override "after_sign_in_path_for" and redirect user to root path if no target URL is stored in session
elsif current_user && session[:user_return_to].nil?
elsif request.format.html? && current_user && session[:user_return_to].nil?
flash.clear
# The authenticated root path can be defined in your routes.rb in: devise_scope :user do...
redirect_to(authenticated_user_root_path) and return
Expand Down
30 changes: 30 additions & 0 deletions app/controllers/users/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
# frozen_string_literal: true

class Users::SessionsController < Devise::SessionsController
# respond_to :json
include Accessible
skip_before_action :check_user, only: :destroy

def create
respond_to do |format|
format.html { super }
format.json {
warden.authenticate!(scope: resource_name, recall: "#{controller_path}#faliure")
render json: "Successfully authenticated", status: 200
}
end
end

# API authentication error
def faliure
render json: "Something went wrong with API sign in :(", status: 401
end

private

def respond_to_on_destroy
# We actually need to hardcode this as Rails default responder doesn't
# support returning empty response on GET request
respond_to do |format|
format.json {
render json: "Successfully Signed Out", status: 200
}
format.all { head :no_content }
format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name), status: Devise.responder.redirect_status }
end
end
end
18 changes: 18 additions & 0 deletions app/models/jwt_denylist.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class JwtDenylist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Denylist

self.table_name = "jwt_denylist"
end

# == Schema Information
#
# Table name: jwt_denylist
#
# id :bigint not null, primary key
# exp :datetime not null
# jti :string not null
#
# Indexes
#
# index_jwt_denylist_on_jti (jti)
#
2 changes: 1 addition & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class User < ApplicationRecord

validates_with UserValidator

devise :database_authenticatable, :invitable, :recoverable, :validatable, :timeoutable, :trackable, :confirmable
devise :database_authenticatable, :invitable, :recoverable, :validatable, :timeoutable, :trackable, :confirmable, :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist

belongs_to :casa_org

Expand Down
3 changes: 3 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ class Application < Rails::Application
config.active_storage.variant_processor = :mini_magick
config.active_storage.content_types_to_serve_as_binary.delete("image/svg+xml")
config.serve_static_assets = true
config.to_prepare do
DeviseController.respond_to :html, :json
end
end
end
2 changes: 1 addition & 1 deletion config/credentials/development.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
svCtLWmi6TUWfy4jhsNxZgGKdzBrjq5JjKkGUaDA5tlP2XFn6XY8lJDVhF+T82kGjwT4EgsBheMZqPMbytlJ6iSDBIq/bHfjl1E5Zx3DqCkd4gDYgVK0roJffesKQPuWUSQUzvJV9pZ9VQEKbh+YA/I/N6aWGbkYlKXTOPHMY7F+rfiKXb8vHodUGWxCTycsWLpe/ohBvF7zzSwxkG7sEmbnRnqYd2Tmn0ASf6vNKXOzPamQ21rrgUss427/zjCjzWHCk4iUaHnhQQYwC2zJ+m1/0Uu+sM5CkYJhddsPbeeQkd7vgPjHBylgkT6L86XTz8sBrQDZB51TbmNouygu96NzQwE472c0csFEWwjz7fepy7sZkHN5KqQ=--dx6D/QqFOeacGYGg--+r3ffqcg8wONL9oMId9u5g==
kCD5s9jBDZZfXdKirYJD95LfwF3xeWi2E+OGS9x9/8EsI6WMzl+jNUjuBQt/fUdO5R5pCHXXPCYPzsGnDyTKJJczqszDXDsG2xS5eSImw9O7BGmNDh7W1sQ5kMmjZPUUu9GaCcAjmnvh0ImXM2X+Lo/5X3NeAZ7FllFmUd3CaBD5J3N3mbXlJgVGRylabaKABqrZqbRnZK5QDOcjwxYspsnu3m9M2qx5B7m2RnpKdytDpxbRtGSj34cZmH0C+rGOCQUm9rSR5wrQVmmJ5DdElYAnM7lo6bUTf2W5wbd51TsrEATPCHx1iK89ot/alUIjmHcoMlUstLnSX8VQzQeIluqa7a7o2IN/p++vknWELVlYROn67wiRTrLPXeDy+NkhiYkhQCBl3AIL8qs92tKX9xwvM75q5pGwsxQneGM5IjOSHUnEmw6j0f6B0R1oksOGt/xg9VP73ANP3zxqNTnbzRNoqO9wVVju0jJVt8KGoXVzdQx2qQSU5ZqUulJ7v88Xjro7Ew6mSY5gh4itzzsVIF7kAEJM20eh2WWxNEDMZyzvKDD34XWxOb8nks9MF0yCJzWBIog=--m9KJMwI1N3qSf0ry--d64JLH6L2BJNNIW2DdMR6w==
2 changes: 1 addition & 1 deletion config/credentials/test.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
zsvjeQzFV0vM7h3jsx7RUyA3WTQsEYWvEMreEvVbi4L0HN7sDNrP7kay2FZS5VEwrW5mJZdnu63BXa8fK/h1agvaaiOO9FhDXfyK+VT86TAfsLa1gsBK9mHjWSdXCJE9TZj9OjOtR3R/qHOI+2uPUnysh7Om4a0ckiu4Jwex3OcbgCYj2+G2JQtwHkWhlyBthGxLjuDDFfx+qxkJWkN7V9FhN0FkkPaflyj4FjR9BUf3/CB8pvHXqJ1lmxVScYsyhh50mc+CKyVptpqbLi9Jou3SiUePREX03ynV0KPR+7mT3FH9gCj4QyzzS1t3JOUfrgqeVFAzdV1TW01olinOyG2aMrZn1aA7GWfDeIr/GwnaPfUMmZNj4RQ=--KmPWCR7xHr6jUSx5--1L2S0bUzD2Bc+JFAHX7xKg==
Z9z6La5/jrIcjvqDXZLmtw2hMblsJ4e+LfUCe3sOPdhdkk7iqSNlEpMe5/HSPubVjpTPuciGifG3W8SCfrESdK3yEYS8VRY1pImPrxBRzE8VjJ6/tyNZ6U/FWfveK2uisnxDeiM1j2PfbM1nhlggMWW+Ll4+LJba7HFQtJn4oyES4dK9syrgcB1zMZeD/Tdqub9eNIUBCqj/ehOnvUnTop6O5k5BI3Z7jXH/iNqNYK+Ks2rq0XdP5C3rLaDaotJkdcWjIZSGraovO6MrDl4YfNdcsWIBWsq2VNgRzkz2zrcaGCXaY9drd4r8OMQiULWplCO6ZAXHeuwubkisVdcqUZMhQ52Pklky36xmri76Tt5ztN30PGiD1PYYceDcHeuqnYsTVEonbJp1o4I7Z0ZvIVSR0dy5TeIne7A=--/Gk/JLOC9ouxL6CR--N9wv5rGQmObNmSe5FsRyjA==
3 changes: 3 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,7 @@
# config.action_cable.disable_request_forgery_protection = true

config.assets.digest = false

# allow requests from ngrok
config.hosts << ".ngrok-free.app"
end
10 changes: 10 additions & 0 deletions config/initializers/cors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "*" # make sure to change to domain name of frontend

resource "*",
headers: %w[Authorization],
methods: :any,
expose: %w[Authorization]
end
end
7 changes: 6 additions & 1 deletion config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@
# should add them to the navigational formats lists.
#
# The "*/*" below is required to match Internet Explorer requests.
# config.navigational_formats = ['*/*', :html]
config.navigational_formats = ["*/*", :html, :json]

# The default HTTP method used to sign out a resource. Default is :delete.
config.sign_out_via = :get
Expand Down Expand Up @@ -345,4 +345,9 @@
# When set to false, does not sign a user in automatically after their password is
# changed. Defaults to true, so a user is signed in automatically after changing a password.
# config.sign_in_after_change_password = true

# ==> Configuration for devise-jwt secret key generation
config.jwt do |jwt|
jwt.secret = Rails.application.credentials.devise.jwt_secret_key
end
end
9 changes: 9 additions & 0 deletions db/migrate/20230308041745_create_jwt_denylist.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class CreateJwtDenylist < ActiveRecord::Migration[7.0]
def change
create_table :jwt_denylist do |t|
t.string :jti, null: false
t.datetime :exp, null: false
end
add_index :jwt_denylist, :jti
end
end
6 changes: 6 additions & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,12 @@
t.index ["casa_org_id"], name: "index_judges_on_casa_org_id"
end

create_table "jwt_denylist", force: :cascade do |t|
t.string "jti", null: false
t.datetime "exp", null: false
t.index ["jti"], name: "index_jwt_denylist_on_jti"
end

create_table "languages", force: :cascade do |t|
t.string "name"
t.bigint "casa_org_id", null: false
Expand Down
64 changes: 64 additions & 0 deletions spec/requests/users/sessions_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
require "rails_helper"

RSpec.describe "Users::SessionsController", type: :request do
let(:casa_org) { create(:casa_org) }
let(:volunteer) { create(:volunteer, casa_org: casa_org) }

describe "POST create" do
before {
@params = {
user: {
email: volunteer.email,
password: volunteer.password
}
}
}

context "when a user signs in" do
context "when request format is json" do
after {
expect(response).not_to have_http_status(:redirect)
expect(response.content_type).to eq("application/json; charset=utf-8")
}

it "respond with jwt in response header" do
# header "Content-Type", "application/json; charset=utf-8"
post "/users/sign_in", as: :json, params: @params
# print request.headers
expect(response.headers).to have_key "Authorization"
expect(response.headers["Authorization"]).to be_starts_with("Bearer")
expect(response.body).to eq "Successfully authenticated"
end

it "respond with status code 401 for bad request" do
post "/users/sign_in", as: :json, params: {user: {email: "suzume@tojimari.jp", password: ""}}
expect(response.headers).not_to have_key "Authorization"
expect(response.body).to eq "Something went wrong with API sign in :("
end
end
end

context "when a user signs out" do
context "when request format is json" do
after { expect(response).not_to have_http_status(:redirect) }

it "adds JWT to denylist" do
# header "Content-Type", "application/json; charset=utf-8"
post "/users/sign_in", as: :json, params: @params
# get JWT from response header
auth_bearer = response.headers["Authorization"]
# print auth_bearer
expect {
get "/users/sign_out", as: :json, headers: {"Authorization" => auth_bearer}
}.to change(JwtDenylist, :count).by(1)
end

it "responds with status code 200" do
# header "Content-Type", "application/json; charset=utf-8"
post "/users/sign_in", as: :json, params: @params
get "/users/sign_out", as: :json
end
end
end
end
end