Skip to content
Closed
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 @@ -24,6 +24,7 @@ mailers/application_mailer.rb
models/application_record.rb
models/concerns/by_organization_scope.rb
models/concerns/roles.rb
models/concerns/generate_token.rb
models/fund_request.rb
models/notification.rb
notifications/base_notification.rb
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ gem "pretender"
gem "puma", "6.3.0" # 6.2.2 fails to install on m1 # Use Puma as the app server
gem "pundit" # for authorization management - based on user.role field
gem "rack-attack" # for blocking & throttling abusive requests
gem "rack-cors" # for allowing cross-origin resource sharing
gem "request_store"
gem "sablon" # Word document templating tool for Case Court Reports
gem "scout_apm"
gem "sprockets-rails" # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "stimulus-rails"
gem "strong_migrations"
gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "active_model_serializers" # for JSON serialization

group :development, :test do
gem "bullet" # Detect and fix N+1 queries
Expand Down
12 changes: 12 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_model_serializers (0.10.13)
actionpack (>= 4.1, < 7.1)
activemodel (>= 4.1, < 7.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.0.6)
activesupport (= 7.0.6)
globalid (>= 0.3.6)
Expand Down Expand Up @@ -118,6 +123,8 @@ GEM
capybara-screenshot (1.0.26)
capybara (>= 1.0, < 4)
launchy
case_transform (0.2)
activesupport
caxlsx (3.4.1)
htmlentities (~> 4.3, >= 4.3.4)
marcel (~> 1.0)
Expand Down Expand Up @@ -244,6 +251,7 @@ GEM
activesupport (>= 5.0.0)
jsbundling-rails (1.1.2)
railties (>= 6.0.0)
jsonapi-renderer (0.2.2)
jwt (2.7.1)
launchy (2.5.0)
addressable (~> 2.7)
Expand Down Expand Up @@ -321,6 +329,8 @@ GEM
rack (2.2.8)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.6)
Expand Down Expand Up @@ -486,6 +496,7 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
active_model_serializers
after_party
amazing_print
annotate
Expand Down Expand Up @@ -531,6 +542,7 @@ DEPENDENCIES
puma (= 6.3.0)
pundit
rack-attack
rack-cors
rails (~> 7.0.5)
rails-controller-testing
rake
Expand Down
23 changes: 23 additions & 0 deletions app/controllers/api/v1/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Api::V1::BaseController < ActionController::API
rescue_from ActiveRecord::RecordNotFound, with: :not_found
before_action :authenticate_user!, except: [:create]

def authenticate_user!
token, options = ActionController::HttpAuthentication::Token.token_and_options(request)
# return nil unless token && options.is_a?(Hash)
print options
user = User.find_by(email: options[:email])
p "#######"
print token
p "#######"
if user && token && ActiveSupport::SecurityUtils.secure_compare(user.token, token)
@current_user = user
else
render json: {message: "Wrong password or email"}, status: 401
end
end

def not_found
api_error(status: 404, errors: "Not found")
end
end
10 changes: 10 additions & 0 deletions app/controllers/api/v1/casa_cases_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Api::V1::CasaCasesController < Api::V1::BaseController
before_action :set_casa_case, only: [:show, :update, :destroy]

# GET /api/v1/casa_cases
def index
@casa_cases = current_user.casa_cases.all
# puts current_user.inspect
render json: @casa_cases, each_serializer: Api::V1::CasaCaseSerializer, status: :ok
end
end
28 changes: 28 additions & 0 deletions app/controllers/api/v1/users/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class Api::V1::Users::SessionsController < Api::V1::BaseController
# POST /api/v1/users/sign_in
def create
load_resource
# p @user
if @user
render json: @user, serializer: Api::V1::SessionSerializer, status: 201
else
render json: {message: "Wrong password or username"}, status: 401
end
end

private

def user_params
params.permit(:email, :password)
end

def load_resource
# print user_params
# print user_params
@user = User.find_by(email: user_params[:email])
if !@user&.valid_password?(user_params[:password])
@user = nil
end
# @user = User.find_by(email: user_params[:email])&.valid_password?(user_params[:password])
end
end
1 change: 1 addition & 0 deletions app/models/casa_admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def change_to_supervisor!
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# token :string
# type :string
# unconfirmed_email :string
# created_at :datetime not null
Expand Down
16 changes: 16 additions & 0 deletions app/models/concerns/generate_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module GenerateToken
extend ActiveSupport::Concern

included do
def ensure_token
self.token = generate_hex(:token) unless token.present?
end

def generate_hex(column)
loop do
hex = SecureRandom.hex(32)
break hex unless self.class.where(column => hex).any?
end
end
end
end
1 change: 1 addition & 0 deletions app/models/supervisor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def recently_unassigned_volunteers
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# token :string
# type :string
# unconfirmed_email :string
# created_at :datetime not null
Expand Down
3 changes: 3 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ class User < ApplicationRecord
include Roles
include ByOrganizationScope
include DateHelper
include GenerateToken

before_update :record_previous_email
after_create :skip_email_confirmation_upon_creation
before_save :normalize_phone_number
before_validation :ensure_token

validates_with UserValidator

Expand Down Expand Up @@ -212,6 +214,7 @@ def normalize_phone_number
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# token :string
# type :string
# unconfirmed_email :string
# created_at :datetime not null
Expand Down
1 change: 1 addition & 0 deletions app/models/volunteer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def cases_where_contact_made_in_days(num_days = CONTACT_MADE_IN_DAYS_NUM)
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# token :string
# type :string
# unconfirmed_email :string
# created_at :datetime not null
Expand Down
16 changes: 16 additions & 0 deletions app/serializers/api/v1/casa_case_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class Api::V1::CasaCaseSerializer < ActiveModel::Serializer
type "casa_case"
attributes :id, :case_number, :casa_org_id, :birthday, :court_report_status, :court_report_submit_date, :youth_date_in_care

def youth_date_in_care
object.date_in_care.strftime("%Y-%m-%d") if object.date_in_care.present?
end

def birthday
object.birth_month_year_youth.strftime("%Y-%m-%d") if object.birth_month_year_youth.present?
end

def court_report_submit_date
object.court_report_submitted_at.strftime("%Y-%m-%d") if object.court_report_submitted_at.present?
end
end
4 changes: 4 additions & 0 deletions app/serializers/api/v1/session_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Api::V1::SessionSerializer < ActiveModel::Serializer
type "session"
attributes :id, :display_name, :email, :token
end
3 changes: 3 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,7 @@

# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
config.hosts << ".ngrok-free.app"

config.assets.digest = false
end
6 changes: 6 additions & 0 deletions config/initializers/cors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "*" # make sure to change to domain name of frontend
resource "/api/v1/*", headers: :any, methods: [:get, :post, :patch, :put, :delete, :options, :head]
end
end
4 changes: 4 additions & 0 deletions config/initializers/rack_attack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class Rack::Attack
end
end

throttle("reg/ip", limit: 5, period: 20.seconds) do |req|
req.ip if req.path.starts_with?("/api/v1")
end

# Throttle POST requests to /xxxx/sign_in by email param
#
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.email}"
Expand Down
9 changes: 9 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,13 @@
end

get "/error", to: "error#index"

namespace :api do
namespace :v1 do
namespace :users do
post "sign_in", to: "sessions#create"
end
get "casa_cases", to: "casa_cases#index"
end
end
end
11 changes: 11 additions & 0 deletions db/migrate/20230710025852_add_token_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class AddTokenToUsers < ActiveRecord::Migration[7.0]
def up
add_column :users, :token, :string
User.find_each { |user| user.save! }
# change_column_null :users, :token, false, 1
end

def down
remove_column :users, :token, :string
end
end
7 changes: 7 additions & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,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 Expand Up @@ -591,6 +597,7 @@
t.string "unconfirmed_email"
t.string "old_emails", default: [], array: true
t.boolean "receive_reimbursement_email", default: false
t.string "token"
t.index ["casa_org_id"], name: "index_users_on_casa_org_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
Expand Down
33 changes: 33 additions & 0 deletions spec/controllers/api/v1/base_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require "rails_helper"

RSpec.describe Api::V1::BaseController, type: :controller do
# assert token and options get parsed correctly
controller do
def index
render json: {message: "Successfully autenticated"}
end

def authenticated_user!
super
end
end

# assert authenticate_user! works
describe "GET #index" do
let(:user) { create(:volunteer) }
it "returns http success" do
request.headers["Authorization"] = "Token token=#{user.token}, email=#{user.email}"
# print request.headers["Authorization"]
get :index
expect(response).to have_http_status(:success)
expect(response.body).to eq({message: "Successfully autenticated"}.to_json)
end
# assert authenticate_user! works with invalid email
it "returns http unauthorized" do
request.headers["Authorization"] = "Token token=, email=#{user.email}"
get :index
expect(response).to have_http_status(:unauthorized)
expect(response.body).to eq({message: "Wrong password or email"}.to_json)
end
end
end
27 changes: 27 additions & 0 deletions spec/controllers/api/v1/casa_cases_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require "rails_helper"
require "spec_helper"

RSpec.describe Api::V1::CasaCasesController, type: :api do
describe "as a volunteer" do
describe "GET /api/v1/casa_cases" do
let(:casa_org) { create(:casa_org) }
let(:volunteer) { create(:volunteer, casa_org: casa_org) }

it "should return a list of volunteer's assigned casa cases" do
casa_cases = create_list(:casa_case, 5, casa_org: casa_org)
create_list(:casa_case, 2, casa_org: casa_org)
volunteer.casa_cases << casa_cases
puts volunteer.casa_cases.inspect
header("Authorization", "Token token=#{volunteer.token}, email=#{volunteer.email}")
get "/api/v1/casa_cases"
expect(last_response.status).to eq 200
expect(last_response.content_type).to eq("application/json; charset=utf-8")

body = JSON.parse(last_response.body, symbolize_names: true)
puts body.inspect
expect(body.length).to eq 5
expect(body).to match_array casa_cases.map { |casa_case| Api::V1::CasaCaseSerializer.new(casa_case).as_json }
end
end
end
end
26 changes: 26 additions & 0 deletions spec/controllers/api/v1/users/sessions_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require "rails_helper"
require "spec_helper"

RSpec.describe Api::V1::Users::SessionsController, type: :api do
let(:casa_org) { create(:casa_org) }
let(:volunteer) { create(:volunteer, casa_org: casa_org) }

it "should handle correct sign in" do
post "/api/v1/users/sign_in", {email: volunteer.email, password: volunteer.password}
# print last_response.headers
# print last_response.body
# expect(last_response.headers).to have_key "Authorization"
# expect(last_response.headers["Authorization"]).to be_starts_with("Bearer")
expect(last_response.body).to eq Api::V1::SessionSerializer.new(volunteer).to_json
expect(last_response.status).to eq 201
expect(last_response.content_type).to eq("application/json; charset=utf-8")
end

it "should handle incorrect sign in" do
post "/api/v1/users/sign_in", {email: "suzume@tojimari.jp", password: ""}
body = JSON.parse(last_response.body, symbolize_names: true)
expect(body.dig(:message)).to eq "Wrong password or username"
expect(last_response.status).to eq 401
expect(last_response.content_type).to eq("application/json; charset=utf-8")
end
end
1 change: 1 addition & 0 deletions spec/factories/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
case_assignments { [] }
phone_number { "" }
confirmed_at { Time.now }
token { "verysecuretoken" }

trait :inactive do
volunteer
Expand Down
Loading