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
2 changes: 2 additions & 0 deletions .allow_skipping_tests
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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 Expand Up @@ -55,3 +56,4 @@ values/casa_admin_parameters.rb
values/supervisor_parameters.rb
values/user_parameters.rb
values/volunteer_parameters.rb
blueprints/api/v1/session_blueprint.rb
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,18 @@ gem "pretender"
gem "puma", "6.4.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 "rswag-api"
gem "rswag-ui"
gem "blueprinter" # for JSON serialization
gem "oj" # faster JSON parsing 🍊

group :development, :test do
gem "bullet" # Detect and fix N+1 queries
Expand All @@ -55,6 +60,7 @@ group :development, :test do
gem "pry"
gem "pry-byebug"
gem "rspec-rails"
gem "rswag-specs"
gem "shoulda-matchers"
gem "standard", "1.5.0" # 1.6.0 errors on all factorybot create variables
end
Expand Down
26 changes: 24 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ GEM
parser (>= 2.4)
smart_properties
bindex (0.8.1)
blueprinter (0.25.3)
brakeman (6.0.1)
bugsnag (6.26.0)
concurrent-ruby (~> 1.0)
Expand Down Expand Up @@ -248,6 +249,8 @@ GEM
activesupport (>= 5.0.0)
jsbundling-rails (1.2.1)
railties (>= 6.0.0)
json-schema (3.0.0)
addressable (>= 2.8)
jwt (2.7.1)
launchy (2.5.0)
addressable (~> 2.7)
Expand Down Expand Up @@ -298,6 +301,7 @@ GEM
noticed (1.6.3)
http (>= 4.0.0)
rails (>= 5.2.0)
oj (3.15.1)
orm_adapter (0.5.0)
parallel (1.23.0)
paranoia (2.6.2)
Expand Down Expand Up @@ -325,8 +329,10 @@ GEM
activesupport (>= 3.0.0)
racc (1.7.1)
rack (2.2.8)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
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.8)
Expand Down Expand Up @@ -387,6 +393,16 @@ GEM
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.0)
rswag-api (2.10.1)
railties (>= 3.1, < 7.1)
rswag-specs (2.10.1)
activesupport (>= 3.1, < 7.1)
json-schema (>= 2.2, < 4.0)
railties (>= 3.1, < 7.1)
rspec-core (>= 2.14)
rswag-ui (2.10.1)
actionpack (>= 3.1, < 7.1)
railties (>= 3.1, < 7.1)
rubocop (1.23.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
Expand Down Expand Up @@ -496,6 +512,7 @@ DEPENDENCIES
amazing_print
annotate
azure-storage-blob
blueprinter
brakeman
bugsnag
bullet
Expand Down Expand Up @@ -529,6 +546,7 @@ DEPENDENCIES
net-pop
net-smtp
noticed
oj
paranoia
pdf-forms
pg
Expand All @@ -538,12 +556,16 @@ DEPENDENCIES
puma (= 6.4.0)
pundit
rack-attack
rack-cors
rails (~> 7.0.8)
rails-controller-testing
rake
request_store
rexml
rspec-rails
rswag-api
rswag-specs
rswag-ui
sablon
scout_apm
selenium-webdriver
Expand Down
5 changes: 5 additions & 0 deletions app/blueprints/api/v1/session_blueprint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Api::V1::SessionBlueprint < Blueprinter::Base
identifier :id

fields :id, :display_name, :email, :token
end
18 changes: 18 additions & 0 deletions app/controllers/api/v1/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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)
user = User.find_by(email: options[:email])
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
23 changes: 23 additions & 0 deletions app/controllers/api/v1/users/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Api::V1::Users::SessionsController < Api::V1::BaseController
def create
load_resource
if @user
render json: Api::V1::SessionBlueprint.render(@user), status: 201
else
render json: {message: "Wrong password or email"}, status: 401
end
end

private

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

def load_resource
@user = User.find_by(email: user_params[:email])
unless @user&.valid_password?(user_params[:password])
@user = nil
end
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 @@ -48,6 +48,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
1 change: 1 addition & 0 deletions app/models/supervisor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class User < ApplicationRecord
before_update :record_previous_email
after_create :skip_email_confirmation_upon_creation
before_save :normalize_phone_number
has_secure_token :token, length: 36

validates_with UserValidator

Expand Down Expand Up @@ -213,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 @@ -168,6 +168,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
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==
aewvdbZoQz8v7s3UlJ/+XOIrxpj1/nP2/dA7FkLGvTgmu8lZrnyecC19sDE6bcZN4XsnIqDomjSg/CL8TefHKXOsaoNNKmW8YPVfoH8AmlqXxvJduiZNuXlOcf7SR01E7E0r1VIdRga6g9KtOHBbgtc6hQyOs/2ajSxbD3gY5IFWnWNHIqMEWMUMy/PXtSSxUr+FdNCgdod9Rx0EEiecfEz1tMBP/V69dRwSrM5yfTeogkUPpOqReFisTbn9f0yolmNhhxo7nPoPzyeEcGHl4+maS1GHa6uYQ2n2d2t34FmhcDttI+rV7ITU9LmuwVcjgCE9fPxMUZ9bX2UBUEHialBZ8S+izXyBAKGTvbQw+/Wk9KNT98Tl3Gg=--BRmMgMTOgyAZUyw4--2OyLty/a3xH0OjlI0sf9Yw==
1 change: 1 addition & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,5 @@

# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
config.hosts << ENV["DEV_HOSTS"]
end
5 changes: 5 additions & 0 deletions config/initializers/blueprinter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require "oj" # you can skip this if OJ has already been required.

Blueprinter.configure do |config|
config.generator = Oj # default is JSON
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
13 changes: 13 additions & 0 deletions config/initializers/rswag_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Rswag::Api.configure do |c|
# Specify a root folder where Swagger JSON files are located
# This is used by the Swagger middleware to serve requests for API descriptions
# NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure
# that it's configured to generate files in the same folder
c.swagger_root = Rails.root.to_s + "/swagger"

# Inject a lambda function to alter the returned Swagger prior to serialization
# The function will have access to the rack env for the current request
# For example, you could leverage this to dynamically assign the "host" property
#
# c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
end
15 changes: 15 additions & 0 deletions config/initializers/rswag_ui.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Rswag::Ui.configure do |c|
# List the Swagger endpoints that you want to be documented through the
# swagger-ui. The first parameter is the path (absolute or relative to the UI
# host) to the corresponding endpoint and the second is a title that will be
# displayed in the document selector.
# NOTE: If you're using rspec-api to expose Swagger files
# (under swagger_root) as JSON or YAML endpoints, then the list below should
# correspond to the relative paths for those endpoints.

c.swagger_endpoint "/api-docs/v1/swagger.yaml", "API V1 Docs"

# Add Basic Auth in case your API is private
# c.basic_auth_enabled = true
# c.basic_auth_credentials 'username', 'password'
end
11 changes: 11 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

Rails.application.routes.draw do
mount Rswag::Ui::Engine => "/api-docs"
mount Rswag::Api::Engine => "/api-docs"
devise_for :all_casa_admins, path: "all_casa_admins", controllers: {sessions: "all_casa_admins/sessions"}
devise_for :users, controllers: {sessions: "users/sessions", passwords: "users/passwords"}

Expand Down Expand Up @@ -189,4 +191,13 @@
end

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

namespace :api do
namespace :v1 do
namespace :users do
post "sign_in", to: "sessions#create"
# get 'sign_out', to: 'sessions#destroy'
end
end
end
end
9 changes: 9 additions & 0 deletions db/migrate/20230710025852_add_token_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class AddTokenToUsers < ActiveRecord::Migration[7.0]
def up
add_column :users, :token, :string
end

def down
remove_column :users, :token, :string
end
end
5 changes: 5 additions & 0 deletions db/migrate/20230822152341_drop_jwt_denylist_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class DropJwtDenylistTable < ActiveRecord::Migration[7.0]
def change
drop_table :jwt_denylist, if_exists: true
end
end
3 changes: 1 addition & 2 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.


ActiveRecord::Schema[7.0].define(version: 2023_09_03_182657) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

Expand Down Expand Up @@ -599,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.boolean "monthly_learning_hours_report", default: false, null: false
t.index ["casa_org_id"], name: "index_users_on_casa_org_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
Expand Down
14 changes: 14 additions & 0 deletions lib/tasks/deployment/20230822145532_populate_api_tokens.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace :after_party do
desc "Deployment task: populate_api_tokens"
task populate_api_tokens: :environment do
puts "Running deploy task 'populate_api_tokens'" unless Rails.env.test?

# Put your task implementation HERE.
User.find_each { |user| user.save! }

# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
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
33 changes: 33 additions & 0 deletions spec/requests/api/v1/base_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require "rails_helper"

RSpec.describe "Base Controller", type: :request do
before do
base_controller = Class.new(Api::V1::BaseController) do
def index
render json: {message: "Successfully autenticated"}
end
end
stub_const("BaseController", base_controller)
Rails.application.routes.disable_clear_and_finalize = true
Rails.application.routes.draw do
get "/index", to: "base#index"
end
end
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is okay for now, but let's remove once we have a route we can actually test authentication against.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright.


after { Rails.application.reload_routes! }

# test authenticate_user! works
describe "GET #index" do
let(:user) { create(:volunteer) }
it "returns http success when valid credentials" do
get "/index", headers: {"Authorization" => "Token token=#{user.token}, email=#{user.email}"}
expect(response).to have_http_status(:success)
expect(response.body).to eq({message: "Successfully autenticated"}.to_json)
end
it "returns http unauthorized if invalid token" do
get "/index", headers: {"Authorization" => "Token token=, email=#{user.email}"}
expect(response).to have_http_status(:unauthorized)
expect(response.body).to eq({message: "Wrong password or email"}.to_json)
end
end
end
Loading