From 8ecf724247903ce89f3c5e747571e697dbd6ffff Mon Sep 17 00:00:00 2001 From: Xihai Luo Date: Tue, 11 Jul 2023 14:52:43 -0400 Subject: [PATCH 1/6] implement api structure; add auth routes --- Gemfile | 2 ++ Gemfile.lock | 17 ++++++++---- app/controllers/api/v1/base_controller.rb | 18 +++++++++++++ .../api/v1/users/sessions_controller.rb | 27 +++++++++++++++++++ app/models/casa_admin.rb | 1 + app/models/supervisor.rb | 1 + app/models/user.rb | 13 +++++++++ app/models/volunteer.rb | 1 + app/serializers/api/v1/session_serializer.rb | 4 +++ config/environments/development.rb | 2 ++ config/initializers/cors.rb | 6 +++++ config/initializers/rack_attack.rb | 4 +++ config/routes.rb | 9 +++++++ .../20230710025852_add_token_to_users.rb | 11 ++++++++ db/schema.rb | 9 ++++++- spec/api/authentication_spec.rb | 26 ++++++++++++++++++ spec/support/api_helper.rb | 11 ++++++++ 17 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 app/controllers/api/v1/base_controller.rb create mode 100644 app/controllers/api/v1/users/sessions_controller.rb create mode 100644 app/serializers/api/v1/session_serializer.rb create mode 100644 config/initializers/cors.rb create mode 100644 db/migrate/20230710025852_add_token_to_users.rb create mode 100644 spec/api/authentication_spec.rb create mode 100644 spec/support/api_helper.rb diff --git a/Gemfile b/Gemfile index fc8de22ade..6ea22ecfc3 100644 --- a/Gemfile +++ b/Gemfile @@ -38,12 +38,14 @@ 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 "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 diff --git a/Gemfile.lock b/Gemfile.lock index f60a1572a7..dca95df1f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -115,6 +120,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) @@ -238,6 +245,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) @@ -264,7 +272,6 @@ GEM method_source (1.0.0) mini_magick (4.11.0) mini_mime (1.1.2) - mini_portile2 (2.8.2) minitest (5.18.1) multi_xml (0.6.0) multipart-post (2.3.0) @@ -280,9 +287,6 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.9) - nokogiri (1.15.2) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) nokogiri (1.15.2-arm64-darwin) racc (~> 1.4) nokogiri (1.15.2-x86_64-darwin) @@ -319,6 +323,8 @@ GEM rack (2.2.7) 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) @@ -478,7 +484,6 @@ PLATFORMS arm64-darwin-20 arm64-darwin-21 arm64-darwin-22 - ruby x86_64-darwin-18 x86_64-darwin-19 x86_64-darwin-20 @@ -487,6 +492,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + active_model_serializers after_party amazing_print annotate @@ -530,6 +536,7 @@ DEPENDENCIES puma (= 6.3.0) pundit rack-attack + rack-cors rails (~> 7.0.5) rails-controller-testing rake diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb new file mode 100644 index 0000000000..cab12c9e68 --- /dev/null +++ b/app/controllers/api/v1/base_controller.rb @@ -0,0 +1,18 @@ +class Api::V1::BaseController < ActionController::API +rescue_from ActiveRecord::RecordNotFound, with: :not_found + + def authenticate_user! + token, options = ActionController::HttpAuthentication::Token.token_and_options(request) + return nil unless token && options.is_a?(hash) + user = User.find_by(email: options[:email]) + if user && ActiveSupport::SecurityUtils.secure_compare(user.token, token) + @current_user = user + else + return UnauthenticatedError + end + end + + def not_found + return api_error(status: 404, errors: 'Not found') + end +end diff --git a/app/controllers/api/v1/users/sessions_controller.rb b/app/controllers/api/v1/users/sessions_controller.rb new file mode 100644 index 0000000000..1858e2964c --- /dev/null +++ b/app/controllers/api/v1/users/sessions_controller.rb @@ -0,0 +1,27 @@ +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 diff --git a/app/models/casa_admin.rb b/app/models/casa_admin.rb index cda73e3160..143ebae127 100644 --- a/app/models/casa_admin.rb +++ b/app/models/casa_admin.rb @@ -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 diff --git a/app/models/supervisor.rb b/app/models/supervisor.rb index ad49691336..c9e2333efc 100644 --- a/app/models/supervisor.rb +++ b/app/models/supervisor.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index bb47cb8ade..b05f03588b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,7 @@ class User < ApplicationRecord before_update :record_previous_email after_create :skip_email_confirmation_upon_creation before_save :normalize_phone_number + before_validation :ensure_token validates_with UserValidator @@ -173,6 +174,17 @@ def after_confirmation send_email_changed_notification end + 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 + private def normalize_phone_number @@ -212,6 +224,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 diff --git a/app/models/volunteer.rb b/app/models/volunteer.rb index 0dbb3e7bb9..5558610ea9 100644 --- a/app/models/volunteer.rb +++ b/app/models/volunteer.rb @@ -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 diff --git a/app/serializers/api/v1/session_serializer.rb b/app/serializers/api/v1/session_serializer.rb new file mode 100644 index 0000000000..1807056fe2 --- /dev/null +++ b/app/serializers/api/v1/session_serializer.rb @@ -0,0 +1,4 @@ +class Api::V1::SessionSerializer < ActiveModel::Serializer + type "session" + attributes :id, :display_name, :email, :token +end diff --git a/config/environments/development.rb b/config/environments/development.rb index 7a63c3c4e3..85ee3c0d7c 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -78,5 +78,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 diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 0000000000..3f261372e0 --- /dev/null +++ b/config/initializers/cors.rb @@ -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 diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 5d84491bfc..b6fee09ced 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -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}" diff --git a/config/routes.rb b/config/routes.rb index 0a7d68cea8..32a60ca21b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -176,4 +176,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 diff --git a/db/migrate/20230710025852_add_token_to_users.rb b/db/migrate/20230710025852_add_token_to_users.rb new file mode 100644 index 0000000000..076f9e9704 --- /dev/null +++ b/db/migrate/20230710025852_add_token_to_users.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 2af08ac6b7..e0285fa682 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_07_04_123327) do +ActiveRecord::Schema[7.0].define(version: 2023_07_10_025852) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -327,6 +327,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 @@ -545,6 +551,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 diff --git a/spec/api/authentication_spec.rb b/spec/api/authentication_spec.rb new file mode 100644 index 0000000000..21ffe6f378 --- /dev/null +++ b/spec/api/authentication_spec.rb @@ -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 diff --git a/spec/support/api_helper.rb b/spec/support/api_helper.rb new file mode 100644 index 0000000000..d4a11cfcbb --- /dev/null +++ b/spec/support/api_helper.rb @@ -0,0 +1,11 @@ +module ApiHelper + include Rack::Test::Methods + + def app + Rails.application + end +end + +RSpec.configure do |config| + config.include ApiHelper, :type=>:api #apply to all spec for apis folder +end From fd4603e9fad54e0317635eaf09e885202aa863e7 Mon Sep 17 00:00:00 2001 From: Xihai Luo Date: Tue, 11 Jul 2023 15:50:27 -0400 Subject: [PATCH 2/6] lint files --- app/controllers/api/v1/base_controller.rb | 6 +++--- app/controllers/api/v1/users/sessions_controller.rb | 11 ++++++----- config/routes.rb | 4 ++-- db/migrate/20230710025852_add_token_to_users.rb | 4 ++-- spec/api/authentication_spec.rb | 10 +++++----- spec/support/api_helper.rb | 2 +- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index cab12c9e68..370aea5e6a 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -1,5 +1,5 @@ class Api::V1::BaseController < ActionController::API -rescue_from ActiveRecord::RecordNotFound, with: :not_found + rescue_from ActiveRecord::RecordNotFound, with: :not_found def authenticate_user! token, options = ActionController::HttpAuthentication::Token.token_and_options(request) @@ -8,11 +8,11 @@ def authenticate_user! if user && ActiveSupport::SecurityUtils.secure_compare(user.token, token) @current_user = user else - return UnauthenticatedError + UnauthenticatedError end end def not_found - return api_error(status: 404, errors: 'Not found') + api_error(status: 404, errors: "Not found") end end diff --git a/app/controllers/api/v1/users/sessions_controller.rb b/app/controllers/api/v1/users/sessions_controller.rb index 1858e2964c..59f8233db7 100644 --- a/app/controllers/api/v1/users/sessions_controller.rb +++ b/app/controllers/api/v1/users/sessions_controller.rb @@ -2,7 +2,7 @@ class Api::V1::Users::SessionsController < Api::V1::BaseController # POST /api/v1/users/sign_in def create load_resource - #p @user + # p @user if @user render json: @user, serializer: Api::V1::SessionSerializer, status: 201 else @@ -11,17 +11,18 @@ def create end private + def user_params params.permit(:email, :password) end def load_resource - #print user_params - #print user_params + # print user_params + # print user_params @user = User.find_by(email: user_params[:email]) - if !(@user&.valid_password?(user_params[:password])) + if !@user&.valid_password?(user_params[:password]) @user = nil end - #@user = User.find_by(email: user_params[:email])&.valid_password?(user_params[:password]) + # @user = User.find_by(email: user_params[:email])&.valid_password?(user_params[:password]) end end diff --git a/config/routes.rb b/config/routes.rb index 32a60ca21b..f215b296fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -180,8 +180,8 @@ namespace :api do namespace :v1 do namespace :users do - post 'sign_in', to: 'sessions#create' - #get 'sign_out', to: 'sessions#destroy' + post "sign_in", to: "sessions#create" + # get 'sign_out', to: 'sessions#destroy' end end end diff --git a/db/migrate/20230710025852_add_token_to_users.rb b/db/migrate/20230710025852_add_token_to_users.rb index 076f9e9704..9d06ae6a54 100644 --- a/db/migrate/20230710025852_add_token_to_users.rb +++ b/db/migrate/20230710025852_add_token_to_users.rb @@ -1,8 +1,8 @@ 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 + User.find_each { |user| user.save! } + # change_column_null :users, :token, false, 1 end def down diff --git a/spec/api/authentication_spec.rb b/spec/api/authentication_spec.rb index 21ffe6f378..58e9c039cc 100644 --- a/spec/api/authentication_spec.rb +++ b/spec/api/authentication_spec.rb @@ -1,16 +1,16 @@ require "rails_helper" require "spec_helper" -RSpec.describe Api::V1::Users::SessionsController , :type => :api do +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") + # 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") diff --git a/spec/support/api_helper.rb b/spec/support/api_helper.rb index d4a11cfcbb..018d8c250b 100644 --- a/spec/support/api_helper.rb +++ b/spec/support/api_helper.rb @@ -7,5 +7,5 @@ def app end RSpec.configure do |config| - config.include ApiHelper, :type=>:api #apply to all spec for apis folder + config.include ApiHelper, type: :api # apply to all spec for apis folder end From 602e4cf1015977e143408af0ed54732f18680502 Mon Sep 17 00:00:00 2001 From: Xihai Luo Date: Mon, 7 Aug 2023 14:42:43 -0400 Subject: [PATCH 3/6] fix some checks like codeclimate and adding extra tests --- app/controllers/api/v1/base_controller.rb | 10 ++++-- app/models/concerns/generate_token.rb | 16 +++++++++ app/models/user.rb | 12 +------ spec/api/controllers/base_controller_spec.rb | 33 +++++++++++++++++++ .../serializers/session_serializer_spec.rb | 20 +++++++++++ ...on_spec.rb => sessions_controller_spec.rb} | 0 spec/factories/users.rb | 1 + spec/models/user_spec.rb | 9 +++++ 8 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 app/models/concerns/generate_token.rb create mode 100644 spec/api/controllers/base_controller_spec.rb create mode 100644 spec/api/serializers/session_serializer_spec.rb rename spec/api/{authentication_spec.rb => sessions_controller_spec.rb} (100%) diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 370aea5e6a..ea4e7ba94d 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -1,14 +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) - return nil unless token && options.is_a?(hash) + # return nil unless token && options.is_a?(Hash) user = User.find_by(email: options[:email]) - if user && ActiveSupport::SecurityUtils.secure_compare(user.token, token) + p "#######" + print token + p "#######" + if user && token && ActiveSupport::SecurityUtils.secure_compare(user.token, token) @current_user = user else - UnauthenticatedError + render json: {message: "Wrong password or email"}, status: 401 end end diff --git a/app/models/concerns/generate_token.rb b/app/models/concerns/generate_token.rb new file mode 100644 index 0000000000..b1e459e978 --- /dev/null +++ b/app/models/concerns/generate_token.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index b05f03588b..fc80076036 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,7 @@ class User < ApplicationRecord include Roles include ByOrganizationScope include DateHelper + include GenerateToken before_update :record_previous_email after_create :skip_email_confirmation_upon_creation @@ -174,17 +175,6 @@ def after_confirmation send_email_changed_notification end - 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 - private def normalize_phone_number diff --git a/spec/api/controllers/base_controller_spec.rb b/spec/api/controllers/base_controller_spec.rb new file mode 100644 index 0000000000..f65989cc20 --- /dev/null +++ b/spec/api/controllers/base_controller_spec.rb @@ -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 diff --git a/spec/api/serializers/session_serializer_spec.rb b/spec/api/serializers/session_serializer_spec.rb new file mode 100644 index 0000000000..60ad79f3ea --- /dev/null +++ b/spec/api/serializers/session_serializer_spec.rb @@ -0,0 +1,20 @@ +require "rails_helper" + +RSpec.describe Api::V1::SessionSerializer, type: :serializer do + before(:each) do + @casa_org = create(:casa_org) + @volunteer = create(:volunteer, casa_org: @casa_org) + @serializer = Api::V1::SessionSerializer.new(@volunteer) + @serialization = ActiveModelSerializers::Adapter.create(@serializer) + end + + subject { JSON.parse(@serialization.to_json) } + + it "should have matching attributes" do + expect(subject["id"]).to eq(@volunteer.id) + expect(subject["email"]).to eq(@volunteer.email) + expect(subject["display_name"]).to eq(@volunteer.display_name) + expect(subject["token"]).to eq(@volunteer.token) + expect(subject.length).to eq(4) + end +end diff --git a/spec/api/authentication_spec.rb b/spec/api/sessions_controller_spec.rb similarity index 100% rename from spec/api/authentication_spec.rb rename to spec/api/sessions_controller_spec.rb diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 8c40fb8fc2..fb21549d15 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -8,6 +8,7 @@ case_assignments { [] } phone_number { "" } confirmed_at { Time.now } + token { "verysecuretoken" } trait :inactive do volunteer diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7679cf071a..92e9d7f7db 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -49,6 +49,15 @@ end end + describe "token generation" do + it "generates a token before validation" do + user = User.new + expect(user.token).to be_nil + user.valid? + expect(user.token).not_to be_nil + end + end + describe "#case_contacts_for" do let(:volunteer) { create(:volunteer, :with_casa_cases) } let(:case_of_interest) { volunteer.casa_cases.first } From 12be2daae93e70e33c11155dfed448049b285dfe Mon Sep 17 00:00:00 2001 From: Xihai Luo Date: Mon, 7 Aug 2023 15:25:40 -0400 Subject: [PATCH 4/6] change file paths of tests so spec checker is satisfied --- .allow_skipping_tests | 1 + .../controllers => controllers/api/v1}/base_controller_spec.rb | 0 .../api/v1/users}/sessions_controller_spec.rb | 0 .../api/v1}/session_serializer_spec.rb | 0 4 files changed, 1 insertion(+) rename spec/{api/controllers => controllers/api/v1}/base_controller_spec.rb (100%) rename spec/{api => controllers/api/v1/users}/sessions_controller_spec.rb (100%) rename spec/{api/serializers => serializers/api/v1}/session_serializer_spec.rb (100%) diff --git a/.allow_skipping_tests b/.allow_skipping_tests index 0d46f29d2b..fdaf9ab7c6 100644 --- a/.allow_skipping_tests +++ b/.allow_skipping_tests @@ -62,6 +62,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 diff --git a/spec/api/controllers/base_controller_spec.rb b/spec/controllers/api/v1/base_controller_spec.rb similarity index 100% rename from spec/api/controllers/base_controller_spec.rb rename to spec/controllers/api/v1/base_controller_spec.rb diff --git a/spec/api/sessions_controller_spec.rb b/spec/controllers/api/v1/users/sessions_controller_spec.rb similarity index 100% rename from spec/api/sessions_controller_spec.rb rename to spec/controllers/api/v1/users/sessions_controller_spec.rb diff --git a/spec/api/serializers/session_serializer_spec.rb b/spec/serializers/api/v1/session_serializer_spec.rb similarity index 100% rename from spec/api/serializers/session_serializer_spec.rb rename to spec/serializers/api/v1/session_serializer_spec.rb From ecdfcb16d1645c61fe2334e21b5ddc0931b3a6d2 Mon Sep 17 00:00:00 2001 From: Xihai Luo Date: Tue, 8 Aug 2023 18:18:03 -0400 Subject: [PATCH 5/6] implement get casa cases endpoint + specs --- app/controllers/api/v1/base_controller.rb | 1 + .../api/v1/casa_cases_controller.rb | 10 +++++++ .../api/v1/casa_case_serializer.rb | 16 +++++++++++ config/routes.rb | 2 +- spec/controllers/api/v1/casa_cases_spec.rb | 27 +++++++++++++++++++ .../api/v1/casa_case_serializer_spec.rb | 20 ++++++++++++++ 6 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/casa_cases_controller.rb create mode 100644 app/serializers/api/v1/casa_case_serializer.rb create mode 100644 spec/controllers/api/v1/casa_cases_spec.rb create mode 100644 spec/serializers/api/v1/casa_case_serializer_spec.rb diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index ea4e7ba94d..ffe6cbf2f5 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -5,6 +5,7 @@ class Api::V1::BaseController < ActionController::API 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 diff --git a/app/controllers/api/v1/casa_cases_controller.rb b/app/controllers/api/v1/casa_cases_controller.rb new file mode 100644 index 0000000000..8dd8990d50 --- /dev/null +++ b/app/controllers/api/v1/casa_cases_controller.rb @@ -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 diff --git a/app/serializers/api/v1/casa_case_serializer.rb b/app/serializers/api/v1/casa_case_serializer.rb new file mode 100644 index 0000000000..15727cd05a --- /dev/null +++ b/app/serializers/api/v1/casa_case_serializer.rb @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 3da503e3b1..c3a7f2a785 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -193,8 +193,8 @@ namespace :v1 do namespace :users do post "sign_in", to: "sessions#create" - # get 'sign_out', to: 'sessions#destroy' end + get "casa_cases", to: "casa_cases#index" end end end diff --git a/spec/controllers/api/v1/casa_cases_spec.rb b/spec/controllers/api/v1/casa_cases_spec.rb new file mode 100644 index 0000000000..fad4e5296f --- /dev/null +++ b/spec/controllers/api/v1/casa_cases_spec.rb @@ -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 diff --git a/spec/serializers/api/v1/casa_case_serializer_spec.rb b/spec/serializers/api/v1/casa_case_serializer_spec.rb new file mode 100644 index 0000000000..b128793673 --- /dev/null +++ b/spec/serializers/api/v1/casa_case_serializer_spec.rb @@ -0,0 +1,20 @@ +require "rails_helper" + +RSpec.describe Api::V1::CasaCaseSerializer, type: :serializer do + before(:each) do + @casa_case = create(:casa_case) + @serializer = Api::V1::CasaCaseSerializer.new(@casa_case) + @serialization = ActiveModelSerializers::Adapter.create(@serializer) + end + + subject { JSON.parse(@serialization.to_json) } + + it "should have matching attributes" do + # match some attributes returned by serializer + expect(subject["id"]).to eq(@casa_case.id) + expect(subject["case_number"]).to eq(@casa_case.case_number) + expect(subject["birthday"]).to eq(@casa_case.birth_month_year_youth.strftime("%Y-%m-%d")) + expect(subject["casa_org_id"]).to eq(@casa_case.casa_org_id) + expect(subject.length).to eq(7) + end +end From a44bddbce37e3dea4f84b58109a8e25d35cd4988 Mon Sep 17 00:00:00 2001 From: Xihai Luo Date: Fri, 11 Aug 2023 14:34:19 -0400 Subject: [PATCH 6/6] change spec name of serializer for spec checker --- .../api/v1/{casa_cases_spec.rb => casa_cases_controller_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/controllers/api/v1/{casa_cases_spec.rb => casa_cases_controller_spec.rb} (100%) diff --git a/spec/controllers/api/v1/casa_cases_spec.rb b/spec/controllers/api/v1/casa_cases_controller_spec.rb similarity index 100% rename from spec/controllers/api/v1/casa_cases_spec.rb rename to spec/controllers/api/v1/casa_cases_controller_spec.rb