Skip to content

Commit

Permalink
Merge pull request #4976 from xihai01/api
Browse files Browse the repository at this point in the history
Initial API Structure + Auth Route
  • Loading branch information
xihai01 authored Oct 16, 2023
2 parents 4d6ad53 + 121c689 commit 7cae9aa
Show file tree
Hide file tree
Showing 28 changed files with 378 additions and 5 deletions.
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

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

0 comments on commit 7cae9aa

Please sign in to comment.