Its been a while
CI / scan_ruby (push) Failing after 2m13s
CI / lint (push) Failing after 20s

This commit is contained in:
Jason Jordan
2026-07-23 10:41:46 -04:00
parent 16fadfd529
commit 9853d58469
83 changed files with 1978 additions and 243 deletions
-1
View File
@@ -1 +0,0 @@
JWT_SECRET_KEY=your_super_secret_key_here
+3 -3
View File
@@ -9,7 +9,7 @@ ARG RUBY_VERSION=3.3.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
# Rails app lives here
WORKDIR /rails
WORKDIR app
# Install base packages
RUN apt-get update -qq && \
@@ -50,7 +50,7 @@ FROM base
# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails
COPY --from=build /app /app
# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
@@ -59,7 +59,7 @@ RUN groupadd --system --gid 1000 rails && \
USER 1000:1000
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
ENTRYPOINT ["/app/bin/docker-entrypoint"]
# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
+10 -3
View File
@@ -3,8 +3,6 @@ source "https://rubygems.org"
ruby "3.3.1"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.2.2", ">= 7.2.2.1"
# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
@@ -45,7 +43,8 @@ end
gem "devise", "~> 4.9"
gem "jwt", "~> 2.10"
# gem "jwt", "~> 2.10"
gem 'devise-jwt'
gem "dotenv-rails", groups: [ :development, :test ]
@@ -54,3 +53,11 @@ gem "rspec-rails", "~> 8.0"
gem "rswag", "~> 2.16"
gem "rack-attack", "~> 6.7"
gem 'activerecord-sqlserver-adapter'
gem 'tiny_tds'
gem 'pundit'
gem 'httparty'
+99
View File
@@ -0,0 +1,99 @@
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.4.8"
# Bundle edge Rails instead:
# gem "rails", github: "rails/rails", branch: "7-2-stable"
gem "rails", "~> 7.2"
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", "~> 6.5"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"
# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 5.3"
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
# gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
# Static analysis for security vulnerabilities [https://brakemanscanner.org/]
gem "brakeman", require: false
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
gem "rubocop-rails-omakase", require: false
gem "bundler-audit"
gem "rspec-rails"
gem "rubocop-rails"
gem "rubocop-rspec"
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem "web-console"
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
gem "rack-mini-profiler"
gem "pry-rails"
end
group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"
gem "selenium-webdriver"
end
gem 'activerecord-sqlserver-adapter'
gem 'tiny_tds'
gem 'devise'
gem 'pundit'
gem "tailwindcss-rails"
gem 'docx'
gem 'httparty'
gem 'combine_pdf'
gem 'pdf-reader'
gem 'rails_icons'
gem 'fastimage'
gem 'rubyzip', require: 'zip'
# gem "solid_queue"
gem 'delayed_job_active_record'
gem 'daemons'
gem 'image_processing'
gem "ruby-vips"
gem 'whenever', require: false
gem 'amatch'
gem 'figaro'
+46 -4
View File
@@ -54,6 +54,9 @@ GEM
activemodel (= 7.2.2.1)
activesupport (= 7.2.2.1)
timeout (>= 0.4.0)
activerecord-sqlserver-adapter (7.2.9)
activerecord (~> 7.2.0)
tiny_tds
activestorage (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
@@ -87,6 +90,7 @@ GEM
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
crass (1.0.6)
csv (3.3.5)
date (3.4.1)
debug (1.10.0)
irb (~> 1.10)
@@ -97,16 +101,33 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-jwt (0.13.0)
devise (>= 4.0.0, < 6.0.0)
warden-jwt_auth (~> 0.10)
diff-lcs (1.6.2)
dotenv (3.1.8)
dotenv-rails (3.1.8)
dotenv (= 3.1.8)
railties (>= 6.1)
drb (2.2.3)
dry-auto_inject (1.2.1)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-configurable (1.4.0)
dry-core (~> 1.0)
zeitwerk (~> 2.6)
dry-core (1.2.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
erb (5.0.1)
erubi (1.13.1)
globalid (1.2.1)
activesupport (>= 6.1)
httparty (0.24.2)
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.0)
@@ -118,7 +139,7 @@ GEM
json-schema (5.1.1)
addressable (~> 2.8)
bigdecimal (~> 3.1)
jwt (2.10.1)
jwt (3.2.0)
base64
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
@@ -135,6 +156,8 @@ GEM
mini_mime (1.1.5)
minitest (5.25.5)
msgpack (1.8.0)
multi_xml (0.9.1)
bigdecimal (>= 3.1, < 5)
net-imap (0.5.8)
date
net-protocol
@@ -166,7 +189,6 @@ GEM
parser (3.3.8.0)
ast (~> 2.4.1)
racc
pg (1.5.9)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
@@ -177,6 +199,8 @@ GEM
public_suffix (6.0.2)
puma (6.6.0)
nio4r (~> 2.0)
pundit (2.5.2)
activesupport (>= 3.0.0)
racc (1.8.1)
rack (3.1.16)
rack-attack (6.7.0)
@@ -296,6 +320,16 @@ GEM
stringio (3.1.7)
thor (1.3.2)
timeout (0.4.3)
tiny_tds (3.4.0)
bigdecimal (>= 2.0.0)
tiny_tds (3.4.0-aarch64-linux-gnu)
bigdecimal (>= 2.0.0)
tiny_tds (3.4.0-aarch64-linux-musl)
bigdecimal (>= 2.0.0)
tiny_tds (3.4.0-x86_64-linux-gnu)
bigdecimal (>= 2.0.0)
tiny_tds (3.4.0-x86_64-linux-musl)
bigdecimal (>= 2.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4)
@@ -304,6 +338,11 @@ GEM
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
warden-jwt_auth (0.12.0)
dry-auto_inject (>= 0.8, < 2)
dry-configurable (>= 0.13, < 2)
jwt (>= 2.1, < 4)
warden (~> 1.2)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
@@ -323,20 +362,23 @@ PLATFORMS
x86_64-linux-musl
DEPENDENCIES
activerecord-sqlserver-adapter
bootsnap
brakeman
debug
devise (~> 4.9)
devise-jwt
dotenv-rails
jwt (~> 2.10)
pg (~> 1.1)
httparty
puma (>= 5.0)
pundit
rack-attack (~> 6.7)
rack-cors
rails (~> 7.2.2, >= 7.2.2.1)
rspec-rails (~> 8.0)
rswag (~> 2.16)
rubocop-rails-omakase
tiny_tds
tzinfo-data
RUBY VERSION
+1
View File
@@ -0,0 +1 @@
app: bin/rails server -b 0.0.0.0 -p 3005
+8 -8
View File
@@ -115,7 +115,7 @@ POST /api/v1/logout
Authorization: Bearer <your_access_token>
```
blacklists the current token (real logout, token becomes invalid)
retires the current token (real logout, token becomes invalid)
## 👥 role-based authorization
@@ -146,7 +146,7 @@ end
## 🔒 security features
* **no secret fallbacks**: JWT_SECRET_KEY must be set (crashes if missing)
* **token blacklisting**: logout actually invalidates tokens
* **token retireing**: logout actually invalidates tokens
* **refresh tokens**: short-lived access tokens (1 hour) + long-lived refresh tokens (7 days)
* **rate limiting**: login, signup, and refresh endpoints are throttled
* **JTI tracking**: every token has a unique identifier for precise control
@@ -176,8 +176,8 @@ rspec
add these to your scheduled jobs (sidekiq, cron, etc):
```ruby
# clean up expired blacklisted tokens
BlacklistedToken.cleanup_expired
# clean up expired retired tokens
RetiredToken.cleanup_expired
# clean up old refresh tokens
RefreshToken.cleanup_old_tokens
@@ -196,8 +196,8 @@ REDIS_URL=your_redis_url (optional, for rack-attack)
### database indexes
migrations include proper indexes for performance:
* `blacklisted_tokens.jti` (unique)
* `blacklisted_tokens.exp`
* `retired_tokens.jti` (unique)
* `retired_tokens.exp`
* `refresh_tokens.token` (unique)
* `refresh_tokens.user_id + revoked`
* `users.role`
@@ -210,7 +210,7 @@ open issues or pull requests.
## ✨ features
* ✅ JWT authentication with secure token generation (includes JTI for tracking)
* ✅ Token blacklisting for real logout (tokens are invalidated on logout)
* ✅ Token retireing for real logout (tokens are invalidated on logout)
* ✅ Refresh tokens (7-day expiry, keeps users logged in securely)
* ✅ Role-based authorization (user, moderator, admin roles)
* ✅ Rate limiting with Rack::Attack (prevents brute force attacks)
@@ -232,7 +232,7 @@ feel free to fork, star, share, or improve.
## ⚠️ disclaimer
this template includes production-grade features like token blacklisting, refresh tokens, and role-based auth.
this template includes production-grade features like token retireing, refresh tokens, and role-based auth.
however, you should still:
* review security settings for your specific use case
* set up proper monitoring and logging
+4 -4
View File
@@ -42,15 +42,15 @@ module Api
end
end
# post /logout -> NOW with real token blacklisting
# adds the current token to blacklist so it can't be used again
# post /logout -> NOW with real token retireing
# adds the current token to retire so it can't be used again
# authorize_request ensures @current_user and token are present
def logout
header = request.headers["Authorization"]
token = header.split(" ").last if header
decoded = JsonWebToken.decode(token)
BlacklistedToken.create!(
RetiredToken.create!(
jti: decoded[:jti],
user_id: decoded[:user_id],
exp: Time.at(decoded[:exp])
@@ -59,7 +59,7 @@ module Api
# also revoke all refresh tokens for this user
@current_user.refresh_tokens.update_all(revoked: true)
render json: { message: "Successfully logged out. Token blacklisted." }, status: :ok
render json: { message: "Successfully logged out. Token retired." }, status: :ok
rescue StandardError => e
render json: { error: "Logout failed: #{e.message}" }, status: :unprocessable_entity
end
@@ -0,0 +1,16 @@
module Api
module V1
class BrokersController < ApplicationController
# before_action :authenticate_user!
def employers_list
broker_id = params[:id]
broker = Baclight::Broker.find(broker_id)
employers = broker.baclight_employers.order(:name).map { |emp| {name: emp.name, entity_key: emp.pl_plan_key }}
render json: employers, status: :ok
end
end
end
end
@@ -0,0 +1,8 @@
module Api
module V1
class CarriersController < ApplicationController
before_action :authenticate_user!
end
end
end
@@ -0,0 +1,18 @@
module Api
module V1
class ClaimsController < ApplicationController
def member_claims
pb_entity_key = params[:id]
member_claims = MemberClaimsService.new(pb_entity_key).call
if member_claims.present?
render json: member_claims, status: :ok
else
render json: { error: "Member Claims not found" }, status: :not_found
end
end
end
end
end
@@ -0,0 +1,36 @@
module Api
module V1
class EmployersController < ApplicationController
# before_action :authenticate_user!
def id_cards
pl_plan_key = params[:id]
id_cards = EmployersService::IdCards.new(pl_plan_key).call
if id_cards.code == 200
content_disposition = id_cards.headers['Content-Disposition']
filename = content_disposition[/filename="?([^"]*)"?/, 1]
# filename = params[:filename].presence || 'employer_idcards.zip'
send_data(
id_cards.body,
type: 'application/zip',
filename: filename,
disposition: 'attachment'
)
else
render json: { error: "Employer not found" }, status: :not_found
end
end
def members_list
pl_plan_key = params[:id]
employer = Baclight::Employer.find_by(pl_plan_key: pl_plan_key)
members = employer.baclight_members.active.order(:name).map { |mem| {name: mem.name, entity_key: mem.pb_entity_key}}
render json: members, status: :ok
end
end
end
end
@@ -0,0 +1,96 @@
module Api
module V1
class IdCardsController < ApplicationController
def member_card
pb_entity_key = params[:id]
layout = params[:layout]
@member = Baclight::Member.find_by(pb_entity_key: pb_entity_key)
# authorize :id_cards, @member
url_components = {
host: ENV["BACLIGHT_SERVER_HOST"],
port: ENV["BACLIGHT_SERVER_PORT"],
path: "/api/v1/web_id_cards/member_card/#{pb_entity_key}/#{layout}"
}
response = HTTParty.get(
URI::HTTP.build(url_components),
headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/pdf' },
stream_body: true
)
if response.code == 200
content_disposition = response.headers['Content-Disposition']
filename = content_disposition[/filename="?([^"]*)"?/, 1]
disposition = layout == "MobileDisplayCard" ? 'inline' : 'attachment'
send_data(
response.body,
type: 'application/pdf',
filename: filename,
disposition: disposition
)
else
render json: { error: "Member not found" }, status: :not_found
end
end
def member_benefits
pb_entity_key = params[:id]
@member = Baclight::Member.find_by(pb_entity_key: pb_entity_key)
plan_id = @member.id_card_plan_id
plan_benefits = Baclight::IdCardPlan.joins(:baclight_id_card_plan_benefits)
.select('id_card_plans.id, id_card_plans.title, id_card_plan_benefits.id, id_card_plan_benefits.sequence, id_card_plan_benefits.benefit, id_card_plan_benefits.benefit_desc')
.where(id: plan_id)
.distinct
if plan_benefits.present?
render json: plan_benefits, status: :ok
else
render json: { error: "Plan Benefits not found" }, status: :not_found
end
end
def employer_cards
pl_plan_key = params[:id]
@employer = Baclight::Employer.find_by(pl_plan_key: pl_plan_key)
# authorize :id_cards, @employer
url_components = {
host: ENV["BACLIGHT_SERVER_HOST"],
port: ENV["BACLIGHT_SERVER_PORT"],
path: "/api/v1/web_id_cards/employer_cards/#{pl_plan_key}"
}
# Send POST with params and enable streaming
response = HTTParty.get(
URI::HTTP.build(url_components),
headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/zip' },
stream_body: true
)
if response.code == 200
content_disposition = response.headers['Content-Disposition']
filename = content_disposition[/filename="?([^"]*)"?/, 1]
# filename = params[:filename].presence || 'employer_idcards.zip'
send_data(
response.body,
type: 'application/zip',
filename: filename,
disposition: 'attachment'
)
else
render json: { error: "Employer not found" }, status: :not_found
end
end
end
end
end
@@ -0,0 +1,74 @@
module Api
module V1
class MembersController < ApplicationController
# before_action :authenticate_user!
def initialize_dashboard
pb_entity_key = params[:id]
@member = Baclight::Member.find_by(pb_entity_key: pb_entity_key)
authorize @member
dashboard_data = MembersService::InitializeDashboard.new(@member).call
render json: dashboard_data, status: :ok
end
def id_card
pb_entity_key = params[:id]
layout = params[:layout]
@member = Baclight::Member.find_by(pb_entity_key: pb_entity_key)
# authorize @member
generated_id_card = MembersService::IdCard.new(@member, layout).call
if generated_id_card.code == 200
content_disposition = generated_id_card.headers['Content-Disposition']
filename = content_disposition[/filename="?([^"]*)"?/, 1]
# filename = params[:filename].presence || 'employer_idcards.zip'
send_data(
generated_id_card.body,
type: 'application/pdf',
filename: filename,
disposition: 'inline'
)
else
render json: { error: "Member not found" }, status: :not_found
end
end
def network_provider
pl_plan_key = params[:id]
network_provider = Baclight::IdCardSetup.find_by(pl_plan_key: pl_plan_key).slice(:network_provider)
if network_provider.present?
render json: network_provider, status: :ok
else
render json: { error: "Network Provider not found" }, status: :not_found
end
end
def plan_benefits
pb_entity_key = params[:id]
plan_benefits = MembersService::PlanBenefits.new(pb_entity_key).call
if plan_benefits.present?
render json: plan_benefits, status: :ok
else
render json: { error: "Plan Benefits not found" }, status: :not_found
end
end
def claims
pb_entity_key = params[:id]
@member = Baclight::Member.find_by(pb_entity_key: pb_entity_key)
recent_claims = MembersService::Claims.new(@member).call
render json: recent_claims, status: :ok
end
end
end
end
@@ -0,0 +1,19 @@
module Api
module V1
class NetworkProviderController < ApplicationController
def employer_network_provider
pl_plan_key = params[:id]
network_provider = Baclight::IdCardSetup.find_by(pl_plan_key: pl_plan_key).slice(:network_provider)
if network_provider.present?
render json: network_provider, status: :ok
else
render json: { error: "Network Provider not found" }, status: :not_found
end
end
end
end
end
@@ -0,0 +1,8 @@
module Api
module V1
class ProvidersController < ApplicationController
before_action :authenticate_user!
end
end
end
@@ -1,2 +1,3 @@
class ApplicationController < ActionController::API
include Pundit::Authorization
end
@@ -19,8 +19,8 @@ module AuthorizeRequest
begin
decoded = JsonWebToken.decode(token)
# check if token is blacklisted (logged out)
if BlacklistedToken.blacklisted?(decoded[:jti])
# check if token is retired (logged out)
if RetiredToken.retired?(decoded[:jti])
render json: { error: "Token has been revoked" }, status: :unauthorized
return
end
@@ -0,0 +1,30 @@
# frozen_string_literal: true
class Users::ConfirmationsController < Devise::ConfirmationsController
# GET /resource/confirmation/new
# def new
# super
# end
# POST /resource/confirmation
# def create
# super
# end
# GET /resource/confirmation?confirmation_token=abcdef
# def show
# super
# end
# protected
# The path used after resending confirmation instructions.
# def after_resending_confirmation_instructions_path_for(resource_name)
# super(resource_name)
# end
# The path used after confirmation.
# def after_confirmation_path_for(resource_name, resource)
# super(resource_name, resource)
# end
end
@@ -0,0 +1,30 @@
# frozen_string_literal: true
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
# You should configure your model like this:
# devise :omniauthable, omniauth_providers: [:twitter]
# You should also create an action method in this controller like this:
# def twitter
# end
# More info at:
# https://github.com/heartcombo/devise#omniauth
# GET|POST /resource/auth/twitter
# def passthru
# super
# end
# GET|POST /users/auth/twitter/callback
# def failure
# super
# end
# protected
# The path used when OmniAuth fails
# def after_omniauth_failure_path_for(scope)
# super(scope)
# end
end
@@ -0,0 +1,34 @@
# frozen_string_literal: true
class Users::PasswordsController < Devise::PasswordsController
# GET /resource/password/new
# def new
# super
# end
# POST /resource/password
# def create
# super
# end
# GET /resource/password/edit?reset_password_token=abcdef
# def edit
# super
# end
# PUT /resource/password
# def update
# super
# end
# protected
# def after_resetting_password_path_for(resource)
# super(resource)
# end
# The path used after sending reset password instructions
# def after_sending_reset_password_instructions_path_for(resource_name)
# super(resource_name)
# end
end
@@ -0,0 +1,105 @@
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
respond_to :json
before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
# def new
# super
# end
# POST /resource
# def create
# super do |resource|
# resource.sync_role_entity!
# end
# end
# GET /resource/edit
# def edit
# super
# end
# PUT /resource
# def update
# super
# end
# DELETE /resource
# def destroy
# super
# end
# GET /resource/cancel
# Forces the session data which is usually expired after sign
# in to be expired now. This is useful if the user wants to
# cancel oauth signing in/up in the middle of the process,
# removing all OAuth session data.
# def cancel
# super
# end
# protected
private
# def respond_with(resource, _opts = {})
# if resource.persisted?
# render json: {
# status: { code: 200, message: 'Signed up successfully.' },
# data: resource
# }, status: :ok
# else
# render json: {
# status: { message: "User couldn't be created successfully. #{resource.errors.full_messages.to_sentence}" }
# }, status: :unprocessable_entity
# end
# end
def respond_with(resource, _opts = {})
if resource.persisted?
# Customize what is returned upon successful login here
render json: {
message: "Signed up successfully.",
user: {
id: resource.id,
email: resource.email,
name: resource.name,
role: resource.role,
role_id: resource.role_id,
jti: resource.jti,
keychain: resource.keychain,
created_at: resource.created_at,
updated_at: resource.updated_at
}
}, status: :ok
else
render json: {
status: { message: "User couldn't be created successfully. #{resource.errors.full_messages.to_sentence}" }
}, status: :unprocessable_entity
end
end
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:role, :role_id, :date_of_birth, :last_four_ssn])
end
# If you have extra params to permit, append them to the sanitizer.
# def configure_account_update_params
# devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
# end
# The path used after sign up.
# def after_sign_up_path_for(resource)
# super(resource)
# end
# The path used after sign up for inactive accounts.
# def after_inactive_sign_up_path_for(resource)
# super(resource)
# end
end
@@ -0,0 +1,49 @@
# frozen_string_literal: true
class Users::SessionsController < Devise::SessionsController
respond_to :json
# POST /resource/sign_in
# def create
# super
# end
# DELETE /resource/sign_out
# def destroy
# super
# end
private
def respond_with(resource, _opts = {})
puts
render json: {
status: { code: 200, message: 'Logged in successfully.' },
user: {
id: resource.id,
email: resource.email,
name: resource.name,
role: resource.role,
role_id: resource.role_id,
jti: resource.jti,
keychain: resource.keychain,
created_at: resource.created_at,
updated_at: resource.updated_at
}
}, status: :ok
end
def respond_to_on_destroy
if current_user
render json: {
status: 200,
message: "Logged out successfully."
}, status: :ok
else
render json: {
status: 401,
message: "Couldn't find an active session."
}, status: :unauthorized
end
end
end
@@ -0,0 +1,30 @@
# frozen_string_literal: true
class Users::UnlocksController < Devise::UnlocksController
# GET /resource/unlock/new
# def new
# super
# end
# POST /resource/unlock
# def create
# super
# end
# GET /resource/unlock?unlock_token=abcdef
# def show
# super
# end
# protected
# The path used after sending unlock password instructions
# def after_sending_unlock_instructions_path_for(resource)
# super(resource)
# end
# The path used after unlocking the resource
# def after_unlock_path_for(resource)
# super(resource)
# end
end
+2
View File
@@ -1,3 +1,5 @@
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
establish_connection :primary
end
+19
View File
@@ -0,0 +1,19 @@
module Baclight
class Broker < BaclightRecord
self.table_name = 'Brokers'
belongs_to :baclight_carrier, class_name: "Baclight::Carrier", foreign_key: "carrier_id"
has_many :baclight_employers, class_name: "Baclight::Employer", foreign_key: "broker_id", primary_key: "id"
has_many :baclight_members,
through: :baclight_employers,
source: :baclight_member
alias_attribute :id, :id
alias_attribute :name, :name
alias_attribute :carrier_id, :carrier_id
alias_attribute :created_at, :created_at
alias_attribute :updated_at, :updated_at
end
end
+20
View File
@@ -0,0 +1,20 @@
module Baclight
class Carrier < BaclightRecord
self.table_name = 'Carriers'
has_many :baclight_brokers, class_name: "Baclight::Broker", foreign_key: "carrier_id", primary_key: "id"
has_many :baclight_employers,
through: :baclight_brokers,
source: :baclight_employer
has_many :baclight_members,
through: :baclight_employers,
source: :baclight_member
alias_attribute :id, :id
alias_attribute :name, :name
alias_attribute :created_at, :created_at
alias_attribute :updated_at, :updated_at
end
end
+31
View File
@@ -0,0 +1,31 @@
module Baclight
class Employer < BaclightRecord
self.table_name = 'Employers'
belongs_to :baclight_broker, class_name: "Baclight::Broker", foreign_key: "broker_id"
has_many :baclight_members, class_name: "Baclight::Member", foreign_key: "employer_id", primary_key: "id"
alias_attribute :id, :id
alias_attribute :name, :name
alias_attribute :slug, :slug
alias_attribute :pl_plan_key, :pl_plan_key
alias_attribute :company_pb_entity_key, :company_pb_entity_key
alias_attribute :plan_id, :plan_id
alias_attribute :group_number, :group_number
alias_attribute :effective_date, :effective_date
alias_attribute :active, :active
alias_attribute :initialized, :initialized
alias_attribute :broker_id, :broker_id
alias_attribute :created_at, :created_at
alias_attribute :updated_at, :updated_at
def belongs_to_levels
self.joins(baclight_broker: :baclight_carrier).select(
'baclight_broker.id AS broker_id',
'baclight_carrier.id AS carrier_id'
)
end
end
end
+18
View File
@@ -0,0 +1,18 @@
module Baclight
class IdCardPlan < BaclightRecord
self.table_name = 'id_card_plans'
has_many :baclight_id_card_plan_benefits, class_name: "Baclight::IdCardPlanBenefit", foreign_key: "plan_id", primary_key: "id"
alias_attribute :id, :id
alias_attribute :title, :title
alias_attribute :pb_product_key, :pb_product_key
alias_attribute :pl_plan_key, :pl_plan_key
alias_attribute :template, :template
alias_attribute :setup_id, :setup_id
alias_attribute :created_at, :created_at
alias_attribute :updated_at, :updated_at
end
end
@@ -0,0 +1,17 @@
module Baclight
class IdCardPlanBenefit < BaclightRecord
self.table_name = 'id_card_plan_benefits'
belongs_to :baclight_id_card_plan, class_name: "Baclight::IdCardPlan", foreign_key: "plan_id"
alias_attribute :id, :id
alias_attribute :benefit_desc, :benefit_desc
alias_attribute :benefit, :benefit
alias_attribute :sequence, :sequence
alias_attribute :plan_id, :plan_id
alias_attribute :created_at, :created_at
alias_attribute :updated_at, :updated_at
end
end
+25
View File
@@ -0,0 +1,25 @@
module Baclight
class IdCardSetup < BaclightRecord
self.table_name = 'id_card_setups'
alias_attribute :id, :id
alias_attribute :print_name, :print_name
alias_attribute :network_provider, :network_provider
alias_attribute :card_template, :card_template
alias_attribute :card_color, :card_color
alias_attribute :rx_group_number, :rx_group_number
alias_attribute :pl_plan_key, :pl_plan_key
alias_attribute :has_divisions, :has_divisions
alias_attribute :has_dental, :has_dental
alias_attribute :active, :active
alias_attribute :initialized, :initialized
alias_attribute :employer_id, :employer_id
alias_attribute :network_logo_id, :network_logo_id
alias_attribute :provider_section_id, :provider_section_id
alias_attribute :rx_section_id, :rx_section_id
alias_attribute :created_at, :created_at
alias_attribute :updated_at, :updated_at
end
end
+44
View File
@@ -0,0 +1,44 @@
module Baclight
class Member < BaclightRecord
self.table_name = 'Members'
belongs_to :baclight_employer, class_name: "Baclight::Employer", foreign_key: "employer_id"
scope :active, -> {
where(active: true)
}
# alias_attribute :id, :id
# alias_attribute :name, :name
# alias_attribute :family_id, :family_id
# alias_attribute :mb_member_key, :mb_member_key
# alias_attribute :pb_entity_key, :pb_entity_key
# alias_attribute :pl_plan_key, :pl_plan_key
# alias_attribute :id_card_display_name, :id_card_display_name
# alias_attribute :coverage_class, :coverage_class
# alias_attribute :division, :division
# alias_attribute :dental_plan_key, :dental_plan_key
# alias_attribute :dependents, :dependents
# alias_attribute :employer_id, :employer_id
# alias_attribute :id_card_plan_id, :id_card_plan_id
# alias_attribute :created_at, :created_at
# alias_attribute :updated_at, :updated_at
# def member_belongs_to
# {
# broker_id: baclight_employer&.broker_id,
# carrier_id: baclight_employer&.baclight_broker&.carrier_id
# }
# end
def broker_id
baclight_employer&.broker_id
end
def carrier_id
baclight_employer&.baclight_broker&.carrier_id
end
end
end
+16
View File
@@ -0,0 +1,16 @@
module Baclight
class Provider < BaclightRecord
self.table_name = 'Providers'
alias_attribute :id, :id
alias_attribute :name, :name
alias_attribute :pb_entity_key, :pb_entity_key
alias_attribute :tax_id, :tax_id
alias_attribute :family_id, :family_id
alias_attribute :provider_group_id, :provider_group_id
alias_attribute :created_at, :created_at
alias_attribute :updated_at, :updated_at
end
end
+4
View File
@@ -0,0 +1,4 @@
class BaclightRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :baclight, reading: :baclight }
end
@@ -1,11 +1,11 @@
class BlacklistedToken < ApplicationRecord
class RetiredToken < ApplicationRecord
belongs_to :user
validates :jti, presence: true, uniqueness: true
validates :exp, presence: true
# check if a token is blacklisted
def self.blacklisted?(jti)
# check if a token is retired
def self.retired?(jti)
exists?(jti: jti)
end
+122 -7
View File
@@ -1,17 +1,132 @@
class User < ApplicationRecord
include Devise::JWT::RevocationStrategies::JTIMatcher
devise :database_authenticatable, :registerable,
:validatable, :jwt_authenticatable, jwt_revocation_strategy: self
# Include default devise modules. Others available are:
# :recoverable, :rememberable, :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:validatable
# associations
has_many :blacklisted_tokens, dependent: :destroy
has_many :refresh_tokens, dependent: :destroy
# role-based authorization
enum :role, { user: 0, admin: 1, moderator: 2 }
enum :role, { member: 0, employer: 1, broker: 2, provider: 3, carrier: 4, admin: 5 }
attr_accessor :keychain
attr_accessor :date_of_birth
attr_accessor :last_four_ssn
# validations so your db doesn't turn into a trash can
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, presence: true, length: { minimum: 6 }, if: -> { new_record? || !password.nil? }
validates :role, presence: true
validates :role_id, presence: true
validate :role_entity_exists, on: :create
# after_initialize :keychain
before_validation :sync_role_entity, on: :create
def keychain
@keychain ||= case role
when "member"
if role_entity.present?
# belongs_to = {
# employer_id: role_entity.employer_id,
# broker_id: role_entity&.baclight_employer.broker_id,
# carrier_id:role_entity&.baclight_employer&.baclight_broker&.carrier_id
# }
keys = role_entity.attributes.with_indifferent_access.slice(
:id, :name, :family_id, :mb_member_key, :pb_entity_key, :pl_plan_key
)
keys #.merge(belongs_to)
end
when "employer"
if role_entity.present?
belongs_to = role_entity.joins(broker: :carrier).select(
'brokers.id AS broker_id',
'carriers.id AS carrier_id'
)
keys = role_entity.attributes.with_indifferent_access.slice(
:name, :pl_plan_key, :company_pb_entity_key, :plan_id, :group_number
)
keys.merge(belongs_to)
end
when "broker"
if role_entity.present?
belongs_to = role_entity.joins(:carrier).select(
'carriers.id AS carrier_id'
)
role_entity.attributes.with_indifferent_access.slice(
:name, :id
).merge(belongs_to)
end
when "provider"
if role_entity.present?
role_entity.attributes.with_indifferent_access.slice(
:name, :pb_entity_key, :tax_id, :family_id
)
end
when "carrier"
if role_entity.present?
role_entity.attributes.with_indifferent_access.slice(
:name, :id
)
end
# when "admin"
end
end
private
def role_entity
@role_entity = case role
when "member"
if self.persisted?
Baclight::Member.find_by(pb_entity_key: role_id)
else
find_initial_member
end
when "employer"
Baclight::Employer.find_by(company_pb_entity_key: role_id)
when "broker"
Baclight::Broker.find_by(broker_id: role_id)
when "provider"
Baclight::Provider.find_by(tax_id: role_id)
when "carrier"
Baclight::Carrier.find_by(carrier_id: role_id)
# when "admin"
end
end
def sync_role_entity
if role == "member"
self.name = role_entity.name.split(",").reverse.join(" ").squish
else
self.name = role_entity.name
end
end
def role_entity_exists
if role_entity.blank?
errors.add(:role, "#{role} with id #{role_id} was not found.")
end
if role == "member"
dob = Vhcs::VwmbMember.find_by(pb_entity_key: role_id).date_of_birth
unless dob == date_of_birth
errors.add(:role, "#{role} with id #{role_id} and #{date_of_birth} was not found.")
end
end
end
def find_initial_member
initial_member = Vhcs::VwmbMember.where("RIGHT(SocialSecurityNumber, 4) = ? AND DateOfBirth = ?", last_four_ssn, date_of_birth).first
self.role_id = initial_member&.pb_entity_key
member = Baclight::Member.find_by(pb_entity_key: role_id)
member.presence
end
end
+103
View File
@@ -0,0 +1,103 @@
module Vhcs
class VwmbMember < VhcsRecord
self.table_name = 'VwmbMember'
alias_attribute :mb_member_key, :MBMemberKey
alias_attribute :pb_entity_key, :PBEntityKey
alias_attribute :pl_plan_key, :PLPlanKey
alias_attribute :mbr_class, :MbrClass
alias_attribute :mbr_type, :MbrType
alias_attribute :mbr_ap_vendor_key, :MbrAPVendorKey
alias_attribute :alt_ap_vendor_key, :AltAPVendorKey
alias_attribute :use_alt_payee, :UseAltPayee
alias_attribute :send_eob_primary, :SendEOBPrimary
alias_attribute :send_eob_dependent, :SendEOBDependent
alias_attribute :send_eob_third_party, :SendEOBThirdParty
alias_attribute :student_flag, :StudentFlag
alias_attribute :disabled_dependent_flag, :DisabledDependentFlag
alias_attribute :is_restricted, :IsRestricted
alias_attribute :third_party_pb_entity_key, :ThirdPartyPBEntityKey
alias_attribute :user_def_1, :UserDef1
alias_attribute :user_def_2, :UserDef2
alias_attribute :family_id, :FamilyID
alias_attribute :sequence_number, :SequenceNumber
alias_attribute :enrollee_type_key, :EnrolleeTypeKey
alias_attribute :enrollee_type, :EnrolleeType
alias_attribute :enrollee_type_value_id, :EnrolleeTypeValueID
alias_attribute :sex_key, :SexKey
alias_attribute :use_primary_address, :UsePrimaryAddress
alias_attribute :birth_date, :BirthDate
alias_attribute :birth_sequence_number, :BirthSequenceNumber
alias_attribute :death_date, :DeathDate
alias_attribute :date_of_birth, :DateOfBirth
alias_attribute :date_of_death, :DateOfDeath
alias_attribute :social_security_number, :SocialSecurityNumber
alias_attribute :hipaaid, :HIPAAID
alias_attribute :company_pb_entity_key, :CompanyPBEntityKey
alias_attribute :entity_type_id, :EntityTypeID
alias_attribute :prefix_id, :PrefixID
alias_attribute :first_name, :FirstName
alias_attribute :middle_name, :MiddleName
alias_attribute :last_name, :LastName
alias_attribute :suffix_id, :SuffixID
alias_attribute :title, :Title
alias_attribute :letter_tag_bit_flags, :LetterTagBitFlags
alias_attribute :full_name_last_name_first, :FullNameLastNameFirst
alias_attribute :policy_number, :PolicyNumber
alias_attribute :send_eob_alt_payee, :SendEOBAltPayee
alias_attribute :send_eob_alt_payee_only, :SendEOBAltPayeeOnly
def attributes
rails_like = {
mb_member_key: self.mb_member_key,
pb_entity_key: self.pb_entity_key,
pl_plan_key: self.pl_plan_key,
mbr_class: self.mbr_class,
mbr_type: self.mbr_type,
mbr_ap_vendor_key: self.mbr_ap_vendor_key,
alt_ap_vendor_key: self.alt_ap_vendor_key,
use_alt_payee: self.use_alt_payee,
send_eob_primary: self.send_eob_primary,
send_eob_dependent: self.send_eob_dependent,
send_eob_third_party: self.send_eob_third_party,
student_flag: self.student_flag,
disabled_dependent_flag: self.disabled_dependent_flag,
is_restricted: self.is_restricted,
third_party_pb_entity_key: self.third_party_pb_entity_key,
user_def_1: self.user_def_1,
user_def_2: self.user_def_2,
family_id: self.family_id,
sequence_number: self.sequence_number,
enrollee_type_key: self.enrollee_type_key,
enrollee_type: self.enrollee_type,
enrollee_type_value_id: self.enrollee_type_value_id,
sex_key: self.sex_key,
use_primary_address: self.use_primary_address,
birth_date: self.birth_date,
birth_sequence_number: self.birth_sequence_number,
death_date: self.death_date,
date_of_birth: self.date_of_birth,
date_of_death: self.date_of_death,
social_security_number: self.social_security_number,
hipaaid: self.hipaaid,
company_pb_entity_key: self.company_pb_entity_key,
entity_type_id: self.entity_type_id,
prefix_id: self.prefix_id,
first_name: self.first_name,
middle_name: self.middle_name,
last_name: self.last_name,
suffix_id: self.suffix_id,
title: self.title,
letter_tag_bit_flags: self.letter_tag_bit_flags,
full_name_last_name_first: self.full_name_last_name_first,
policy_number: self.policy_number,
send_eob_alt_payee: self.send_eob_alt_payee,
send_eob_alt_payee_only: self.send_eob_alt_payee_only,
}
super.merge(rails_like)
end
end
end
+4
View File
@@ -0,0 +1,4 @@
class VhcsRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :vhcs, reading: :vhcs }
end
+53
View File
@@ -0,0 +1,53 @@
# frozen_string_literal: true
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
@user = user
@record = record
end
def index?
false
end
def show?
false
end
def create?
false
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
class Scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
raise NoMethodError, "You must define #resolve in #{self.class}"
end
private
attr_reader :user, :scope
end
end
+13
View File
@@ -0,0 +1,13 @@
class Baclight::BrokerPolicy < ApplicationPolicy
def employers_list?; true; end
private
def broker_rule?
user.admin? ||
(user.broker? && record.broker_id == user.keychain[:id]) ||
(user.carrier? && record.carrier_id == user.keychain[:id])
end
end
+15
View File
@@ -0,0 +1,15 @@
class Baclight::EmployerPolicy < ApplicationPolicy
def id_cards?; employer_rule?; end
def members_list?; true; end
private
def employer_rule?
user.admin? ||
(user.employer? && record.employer_id == user.keychain[:id]) ||
(user.broker? && record.broker_id == user.keychain[:id]) ||
(user.carrier? && record.carrier_id == user.keychain[:id])
end
end
+22
View File
@@ -0,0 +1,22 @@
class Baclight::MemberPolicy < ApplicationPolicy
def initialize_dashboard?; member_rule?; end
def id_card?; member_rule?; end
def claims?
user.admin? ||
(user.member? && record.id == user.keychain[:id]) ||
(user.carrier? && record.carrier_id == user.keychain[:id])
end
private
def member_rule?
user.admin? ||
(user.member? && record.id == user.keychain[:id]) ||
(user.employer? && record.employer_id == user.keychain[:id]) ||
(user.broker? && record.broker_id == user.keychain[:id]) ||
(user.carrier? && record.carrier_id == user.keychain[:id])
end
end
+18
View File
@@ -0,0 +1,18 @@
class IdCardsPolicy < ApplicationPolicy
# Corresponds to PostController#edit or #update
def member_card?
user.admin? ||
(user.carrier? && record.belongs_to_levels.carrier_id == user.keychain.id) ||
(user.broker? && record.belongs_to_levels.broker_id == user.keychain.id) ||
(user.employer? && record.pl_plan_key == user.keychain.pl_plan_key) ||
(user.member? && record.pb_entity_key == user.keychain.pb_entity_key)
end
# Corresponds to PostController#destroy
def employer_cards?
user.admin? ||
(user.carrier? && record.belongs_to_levels.carrier_id == user.keychain.id) ||
(user.broker? && record.belongs_to_levels.broker_id == user.keychain.id) ||
(user.employer? && record.pl_plan_key == user.keychain.pl_plan_key)
end
end
+14
View File
@@ -0,0 +1,14 @@
class CallStoredProc
def initialize(procedure_name, args = {})
@procedure_name = procedure_name
@args = args
end
def call
params_sql = @args.map { |key, value| "@#{key} = :#{key}" }.join(', ')
sql_template = "EXEC #{@procedure_name} #{params_sql}"
sanitized_sql = VhcsRecord.send(:sanitize_sql_array, [sql_template, @args])
VhcsRecord.connection.exec_query(sanitized_sql)
end
end
@@ -0,0 +1,41 @@
module EmployersService
class IdCards
def initialize(pl_plan_key)
@employer = Baclight::Employer.find_by(pl_plan_key: pl_plan_key)
if @employer.present?
@pl_plan_key = pl_plan_key
else
raise ArgumentError, "Member not found."
end
end
def call
url_components = {
host: ENV["BACLIGHT_SERVER_HOST"],
port: ENV["BACLIGHT_SERVER_PORT"],
path: "/api/v1/web_id_cards/employer_cards/#{@pl_plan_key}"
}
response = HTTParty.get(
URI::HTTP.build(url_components),
headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/zip' },
stream_body: true
)
if response.code == 200
# content_disposition = response.headers['Content-Disposition']
# {
# zip_file: response.body,
# filename: content_disposition[/filename="?([^"]*)"?/, 1]
# }
response
else
nil
end
end
end
end
+33
View File
@@ -0,0 +1,33 @@
class MemberClaimsService
def initialize(pb_entity_key)
@member = Baclight::Member.find_by(pb_entity_key: pb_entity_key)
if @member.present?
@pl_plan_key = @member.pl_plan_key
@family_id = @member.family_id
else
raise ArgumentError, "Member not found."
end
end
def call
args = {
PLPlanKey: @pl_plan_key,
FamilyId: @family_id,
ProviderTaxId: "",
UserID: "JASONJ"
}
claims_headers = CallStoredProc.new("CPFindClaim", args).call
recent_claims_headers = claims_headers.sort_by { |ch| ch['IncurDate'].to_date }.reverse.first(6)
recent_claims_headers.map { |ch| { claim_number: ch['CPHeaderKey'], visit_date: ch['IncurDate'].strftime("%m-%d-%Y"), claim_status: ch['AdjudicatedStatusShortDesc'], patient_name: ch['FullName'], provider_name: ch['ProviderCompany']} }
end
end
# claimHeaders = _vContext.ClaimHeader.FromSql("dbo.CPFindClaim
# @PLPlanKey = {0}, @CSCaseKey = {1}, @PAPriorAuthKey = {2}, @FromDate = {3}, @ToDate = {4}, @FamilyID = {5}, @FirstName = {6}, @LastName = {7}, @SequenceNumber = {8}, @PBEntityKey = {9}, @ProviderIdentifierCode = {10}, @ProviderCompanyName = {11}, @ProviderTaxID = {12}, @HeaderStatusValueID = {13}, @AdjudicatedStatusValueID = {14}, @EventUSUserKey = {15}, @IncludeRestricted = {16}, @ClaimTypeValueID = {17}, @ClaimSource = {18}, @ReceivedFromDate = {19}, @ReceivedToDate = {20}, @DCN = {21}, @SSN = {22}, @SSN_IncFamily = {23}, @SearchAllIDs = {24}, @UserID = {25}, @ExecuteOrDisplay = {26} ",
# PlanKey, 0, 0, FromDate, ToDate, FamilyId, "", "", -1, 0, "", "", ProviderTaxId, 0, 0, 0, 0, 0, 2, null, null, null, null, 0, 0, "DAVIDH", 1)
+41
View File
@@ -0,0 +1,41 @@
module MembersService
class Claims
def initialize(entity, recent = false)
if entity.is_a?(Integer)
@member = Baclight::Member.find_by(pb_entity_key: entity)
else
@member = entity
end
if @member.present?
@pl_plan_key = @member.pl_plan_key
@family_id = @member.family_id
@recent = recent
else
raise ArgumentError, "Member not found."
end
end
def call
args = {
PLPlanKey: @pl_plan_key,
FamilyId: @family_id,
ProviderTaxId: "",
UserID: "JASONJ"
}
claims_headers = CallStoredProc.new("CPFindClaim", args).call
ordered_claims_headers = claims_headers.sort_by { |ch| ch['IncurDate'].to_date }.reverse
ordered_claims_headers = ordered_claims_headers.first(6) if @recent
ordered_claims_headers.map { |ch| { claim_number: ch['CPHeaderKey'], visit_date: ch['IncurDate'].strftime("%m-%d-%Y"), claim_status: ch['AdjudicatedStatusShortDesc'], patient_name: ch['FullName'], provider_name: ch['ProviderCompany']} } || []
end
end
end
# claimHeaders = _vContext.ClaimHeader.FromSql("dbo.CPFindClaim
# @PLPlanKey = {0}, @CSCaseKey = {1}, @PAPriorAuthKey = {2}, @FromDate = {3}, @ToDate = {4}, @FamilyID = {5}, @FirstName = {6}, @LastName = {7}, @SequenceNumber = {8}, @PBEntityKey = {9}, @ProviderIdentifierCode = {10}, @ProviderCompanyName = {11}, @ProviderTaxID = {12}, @HeaderStatusValueID = {13}, @AdjudicatedStatusValueID = {14}, @EventUSUserKey = {15}, @IncludeRestricted = {16}, @ClaimTypeValueID = {17}, @ClaimSource = {18}, @ReceivedFromDate = {19}, @ReceivedToDate = {20}, @DCN = {21}, @SSN = {22}, @SSN_IncFamily = {23}, @SearchAllIDs = {24}, @UserID = {25}, @ExecuteOrDisplay = {26} ",
# PlanKey, 0, 0, FromDate, ToDate, FamilyId, "", "", -1, 0, "", "", ProviderTaxId, 0, 0, 0, 0, 0, 2, null, null, null, null, 0, 0, "DAVIDH", 1)
+41
View File
@@ -0,0 +1,41 @@
module MembersService
class IdCard
def initialize(member, layout)
@member = member
if @member.present?
@pb_entity_key = @member.pb_entity_key
@layout = layout
else
raise ArgumentError, "Member not found."
end
end
def call
url_components = {
host: ENV["BACLIGHT_SERVER_HOST"],
port: ENV["BACLIGHT_SERVER_PORT"],
path: "/api/v1/web_id_cards/member_card/#{@pb_entity_key}/#{@layout}"
}
response = HTTParty.get(
URI::HTTP.build(url_components),
headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/pdf' },
stream_body: true
)
if response.code == 200
# content_disposition = response.headers['Content-Disposition']
# {
# pdf_file: response.body,
# filename: content_disposition[/filename="?([^"]*)"?/, 1]
# }
response
else
nil
end
end
end
end
@@ -0,0 +1,24 @@
module MembersService
class InitializeDashboard
def initialize(member)
@member = member
unless @member.present?
raise ArgumentError, "Member not found."
end
end
def call
plan_benefits = MembersService::PlanBenefits.new(@member).call
network_provider = MembersService::NetworkProvider.new(@member).call
recent_claims = MembersService::Claims.new(@member, true).call
{
network_provider: network_provider,
plan_benefits: plan_benefits,
recent_claims: recent_claims
}
end
end
end
@@ -0,0 +1,18 @@
module MembersService
class NetworkProvider
def initialize(member)
@member = member
if @member.present?
@pl_plan_key = @member.pl_plan_key
else
raise ArgumentError, "Member not found."
end
end
def call
Baclight::IdCardSetup.find_by(pl_plan_key: @pl_plan_key).network_provider
end
end
end
@@ -0,0 +1,20 @@
module MembersService
class PlanBenefits
def initialize(member)
@member = member
if @member.present?
@plan_id = @member.id_card_plan_id
else
raise ArgumentError, "Member not found."
end
end
def call
Baclight::IdCardPlan.joins(:baclight_id_card_plan_benefits)
.select('id_card_plans.id, id_card_plans.title, id_card_plan_benefits.id, id_card_plan_benefits.sequence, id_card_plan_benefits.benefit, id_card_plan_benefits.benefit_desc')
.where(id: @plan_id)
.distinct
end
end
end
Executable → Regular
View File
Executable → Regular
View File
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env sh
if ! gem list foreman -i --silent; then
echo "Installing foreman..."
gem install foreman
fi
# Default to port 3000 if not specified
export PORT="${PORT:-3005}"
# Let the debug gem allow remote connections,
# but avoid loading until `debugger` is called
export RUBY_DEBUG_OPEN="true"
export RUBY_DEBUG_LAZY="true"
exec foreman start -f Procfile.dev "$@"
Executable → Regular
+7
View File
@@ -10,4 +10,11 @@ if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
./bin/rails db:prepare
fi
rm -f /usr/src/app/tmp/pids/server.pid
rm -rf /usr/local/bundle/cache/*.gem
echo "bundle install..."
bundle check || bundle install --jobs 4
exec "${@}"
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
+7
View File
@@ -41,5 +41,12 @@ module RailsApiAuthTemplate
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
config.middleware.use Rack::Attack
# 1. Configure a session store type and key names
config.session_store :cookie_store, key: '_britton_api_session'
# 2. Re-include the required middlewares for cookies and sessions
config.middleware.use ActionDispatch::Cookies
config.middleware.use config.session_store, config.session_options
end
end
+92 -75
View File
@@ -1,85 +1,102 @@
# PostgreSQL. Versions 9.3 and up are supported.
#
# Install the pg driver:
# gem install pg
# On macOS with Homebrew:
# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
# On Windows:
# gem install pg
# Choose the win32 build.
# Install PostgreSQL and put its /bin directory on your path.
#
# Configure Using Gemfile
# gem "pg"
#
default: &default
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# https://guides.rubyonrails.org/configuring.html#database-pooling
production:
primary:
adapter: sqlserver
mode: dblib
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
host: 10.41.82.73
port: 1433
database: BrittonWeb
username: BSTI
password: BSTIBOY
tds_version: 7.3
vhcs:
adapter: sqlserver
mode: dblib
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
host: "10.41.82.72"
port: 1433
database: VHCS_HIPAA
username: BSTI
password: BSTIBOY
database_tasks: false
tds_version: 7.3
baclight:
adapter: sqlserver
mode: dblib
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
host: "10.41.82.73"
port: 1433
database: Baclight
username: BSTI
password: BSTIBOY
database_tasks: false
tds_version: 7.3
heb_web:
adapter: sqlserver
mode: dblib
host: 10.41.82.73
port: 1433
database: HEBWeb
username: SA
password: Adm1nBb5
pool: 5
database_tasks: false
tds_version: 7.3
development:
<<: *default
database: rails_api_auth_template_development
primary:
adapter: sqlserver
mode: dblib
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
# The specified database role being used to connect to PostgreSQL.
# To create additional roles in PostgreSQL see `$ createuser --help`.
# When left blank, PostgreSQL will use the default role. This is
# the same name as the operating system user running Rails.
#username: rails_api_auth_template
host: 10.41.82.73
port: 1433
database: BrittonWeb
username: BSTI
password: BSTIBOY
tds_version: 7.3
# The password associated with the PostgreSQL role (username).
#password:
vhcs:
adapter: sqlserver
mode: dblib
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
# Connect on a TCP socket. Omitted by default since the client uses a
# domain socket that doesn't need configuration. Windows does not have
# domain sockets, so uncomment these lines.
#host: localhost
host: "10.41.82.73"
port: 1433
database: VHCS_HIPAA
username: BSTI
password: BSTIBOY
database_tasks: false
tds_version: 7.3
# The TCP port the server listens on. Defaults to 5432.
# If your server runs on a different port number, change accordingly.
#port: 5432
baclight:
adapter: sqlserver
mode: dblib
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
# Schema search path. The server defaults to $user,public
#schema_search_path: myapp,sharedapp,public
host: "10.41.82.73"
port: 1433
database: Baclight
username: BSTI
password: BSTIBOY
database_tasks: false
tds_version: 7.3
# Minimum log levels, in increasing order:
# debug5, debug4, debug3, debug2, debug1,
# log, notice, warning, error, fatal, and panic
# Defaults to warning.
#min_messages: notice
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: rails_api_auth_template_test
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
# Instead, provide the password or a full connection URL as an environment
# variable when you boot the app. For example:
#
# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
#
# If the connection URL is provided in the special DATABASE_URL environment
# variable, Rails will automatically merge its configuration values on top of
# the values provided in this file. Alternatively, you can specify a connection
# URL environment variable explicitly:
#
# production:
# url: <%= ENV["MY_APP_DATABASE_URL"] %>
#
# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
# for a full overview on how database connection configuration can be specified.
#
production:
<<: *default
database: rails_api_auth_template_production
username: rails_api_auth_template
password: <%= ENV["RAILS_API_AUTH_TEMPLATE_DATABASE_PASSWORD"] %>
heb_web:
adapter: sqlserver
mode: dblib
host: 10.41.82.73
port: 1433
database: HEBWeb
username: SA
password: Adm1nBb5
pool: 5
database_tasks: false
tds_version: 7.3
+2
View File
@@ -17,6 +17,8 @@ Rails.application.configure do
# Enable server timing.
config.server_timing = true
config.hosts << "web_api"
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
+3 -2
View File
@@ -7,10 +7,11 @@
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "*"
origins 'http://localhost:3001'
resource "*",
headers: :any,
methods: [ :get, :post, :put, :patch, :delete, :options, :head ]
methods: [ :get, :post, :put, :patch, :delete, :options, :head ],
credentials: true
end
end
+11
View File
@@ -16,6 +16,17 @@ Devise.setup do |config|
# by default. You can change it below and use your own secret key.
# config.secret_key = '48e5fef13ecab7262e95624bd16f02e6a4ce11f2d44f1e18851e3e046535a6b6c233c94d1eb28e62655a12a63acb9ead3426ca1cb36753fa33f2970cd5377c3b'
config.jwt do |jwt|
jwt.secret = ENV['DEVISE_JWT_SECRET_KEY'] # Or Rails.application.credentials.devise_jwt_secret_key!
jwt.dispatch_requests = [
['POST', %r{^/login$}]
]
jwt.revocation_requests = [
['DELETE', %r{^/logout$}]
]
jwt.expiration_time = 30.minutes.to_i
end
# ==> Controller configuration
# Configure the parent class to the devise controllers.
# config.parent_controller = 'DeviseController'
@@ -4,5 +4,5 @@
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
:passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
:secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
]
+45 -7
View File
@@ -1,7 +1,17 @@
Rails.application.routes.draw do
mount Rswag::Ui::Engine => "/api-docs"
mount Rswag::Api::Engine => "/api-docs"
devise_for :users, skip: [ :registrations, :passwords, :confirmations ]
devise_for :users,
path: '',
path_names: {
sign_in: 'signin',
sign_out: 'signout',
registration: 'signup'
},
controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations'
}
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
@@ -10,16 +20,44 @@ Rails.application.routes.draw do
namespace :api do
namespace :v1 do
post "signup", to: "auth#signup"
post "login", to: "auth#login"
post "logout", to: "auth#logout"
post "refresh", to: "auth#refresh"
# post "signup", to: "auth#signup"
# post "login", to: "auth#login"
# post "logout", to: "auth#logout"
# post "refresh", to: "auth#refresh"
get "protected", to: "protected#index"
get "profile", to: "profile#show"
# get "protected", to: "protected#index"
# get "profile", to: "profile#show"
# admin routes
get "admin/dashboard", to: "admin#dashboard"
get 'id_cards/member_card/:id/:layout', to: 'id_cards#member_card'
get 'id_cards/member_benefits/:id', to: 'id_cards#member_benefits'
get 'id_cards/employer_cards/:id', to: 'id_cards#employer_cards'
get 'claims/member_claims/:id', to: 'claims#member_claims'
get 'network_provider/employer_network_provider/:id', to: 'network_provider#employer_network_provider'
resources :members do
member do
get 'initialize_dashboard'
get 'id_card/:layout', to: 'members#id_card'
get 'claims'
end
end
resources :employers do
member do
get 'initialize_dashboard'
get 'id_cards'
get 'members_list'
end
end
resources :brokers do
member do
get 'employers_list'
end
end
end
end
end
@@ -7,6 +7,12 @@ class DeviseCreateUsers < ActiveRecord::Migration[7.2]
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
t.string :name, default: ""
t.integer :role, null: false
t.integer :role_id, null: false
t.string :jti, null: false
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
@@ -37,7 +43,8 @@ class DeviseCreateUsers < ActiveRecord::Migration[7.2]
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
add_index :users, :reset_password_token, unique: false
add_index :users, :jti, unique: true
# add_index :users, :confirmation_token, unique: true
# add_index :users, :unlock_token, unique: true
end
@@ -1,14 +0,0 @@
class CreateBlacklistedTokens < ActiveRecord::Migration[7.2]
def change
create_table :blacklisted_tokens do |t|
t.string :jti, null: false
t.references :user, null: false, foreign_key: true
t.datetime :exp, null: false
t.timestamps
end
add_index :blacklisted_tokens, :jti, unique: true
add_index :blacklisted_tokens, :exp
end
end
@@ -1,6 +0,0 @@
class AddRoleToUsers < ActiveRecord::Migration[7.2]
def change
add_column :users, :role, :integer, default: 0, null: false
add_index :users, :role
end
end
@@ -0,0 +1,14 @@
class CreateRetiredTokens < ActiveRecord::Migration[7.2]
def change
create_table :retired_tokens do |t|
t.string :jti, null: false
t.references :user, null: false, foreign_key: true
t.datetime :exp, null: false
t.timestamps
end
add_index :retired_tokens, :jti, unique: true
add_index :retired_tokens, :exp
end
end
@@ -0,0 +1,9 @@
class AddRoleAndNameToUsers < ActiveRecord::Migration[7.2]
def change
# add_column :users, :role, :integer, default: 0, null: false
# add_column :users, :role_id, :integer, null: false
# add_column :users, :name, :string, null: false
# add_index :users, :role
end
end
Generated
+7 -33
View File
@@ -10,47 +10,21 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2026_01_28_000003) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "blacklisted_tokens", force: :cascade do |t|
t.string "jti", null: false
t.bigint "user_id", null: false
t.datetime "exp", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["exp"], name: "index_blacklisted_tokens_on_exp"
t.index ["jti"], name: "index_blacklisted_tokens_on_jti", unique: true
t.index ["user_id"], name: "index_blacklisted_tokens_on_user_id"
end
create_table "refresh_tokens", force: :cascade do |t|
t.string "token", null: false
t.bigint "user_id", null: false
t.datetime "expires_at", null: false
t.boolean "revoked", default: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["token"], name: "index_refresh_tokens_on_token", unique: true
t.index ["user_id", "revoked"], name: "index_refresh_tokens_on_user_id_and_revoked"
t.index ["user_id"], name: "index_refresh_tokens_on_user_id"
end
ActiveRecord::Schema[7.2].define(version: 2025_06_13_174349) do
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "name", default: ""
t.integer "role", null: false
t.integer "role_id", null: false
t.string "jti", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "role", default: 0, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["role"], name: "index_users_on_role"
t.index ["jti"], name: "index_users_on_jti", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token"
end
add_foreign_key "blacklisted_tokens", "users"
add_foreign_key "refresh_tokens", "users"
end
+34
View File
@@ -0,0 +1,34 @@
# ----- Build Stage -----
# Using a specific Ruby version (e.g., 3.3) for stability
ARG RUBY_VERSION=3.3.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips tdsodbc freetds-dev build-essential libpq-dev libyaml-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
ENV LANG=C.UTF-8 \
BUNDLE_JOBS=4 \
BUNDLE_RETRY=3
WORKDIR /usr/src/app
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails /usr/src/app
USER 1000:1000
# COPY bin/development-entrypoint.sh /bin/development-entrypoint.sh
# RUN chown root:root bin/development-entrypoint.sh && chmod +x bin/development-entrypoint.sh
ENTRYPOINT ["./bin/docker-entrypoint"]
# ENTRYPOINT ["./bin/cron-entrypoint"]
# Expose the application port
EXPOSE 3005
# Set the default command to run the Rails server
CMD ["./bin/dev"]
+31
View File
@@ -0,0 +1,31 @@
services:
app:
build:
context: ./
dockerfile: development.Dockerfile
container_name: web_api
volumes:
- .:/usr/src/app
- bundle:/usr/local/bundle
ports:
- "3005:3005"
environment:
- HISTFILE=/usr/src/app/log/.bash_history
- RAILS_ENV=development
- BACLIGHT_SERVER_HOST=baclight
- BACLIGHT_SERVER_PORT=3002
- PUID=1000
- PGID=100
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
- DEVISE_JWT_SECRET_KEY=${DEVISE_JWT_SECRET_KEY}
tty: true
stdin_open: true
networks:
- britton_network
networks:
britton_network:
external: true
volumes:
bundle:
+8
View File
@@ -0,0 +1,8 @@
Description:
Explain the generator
Example:
bin/rails generate legacy_db_model Thing
This will create:
what/will/it/create
@@ -0,0 +1,83 @@
class LegacyDbModelGenerator < Rails::Generators::Base
source_root File.expand_path('templates', __dir__)
argument :db_name, type: :string, required: true
argument :table_name, type: :string, required: true
def determine_db_record
if db_name == 'VHCS_HIPAA'
@db_record = 'VhcsRecord'.constantize
@module = 'Vhcs'.constantize
@file_folder = 'vhcs'
elsif db_name == 'Baclight'
@db_record = 'BaclightRecord'.constantize
@module = 'Baclight'.constantize
@file_folder = 'baclight'
elsif db_name == 'HEBWeb'
@db_record = 'HebWebRecord'.constantize
@module = 'HebWeb'.constantize
@file_folder = 'heb_web'
else
raise LegacyDBModelError, "Invalid database, please use VHCS_HIPAA or HEBWeb"
end
end
def get_fields_if_table_exists
tn = table_name.strip
if @db_record.connection.table_exists?(tn) || @db_record.connection.view_exists?(table_name)
@fields = @db_record.connection.columns(tn).map { |col| col.name }
else
raise LegacyDBModelError, "Invalid table/view for #{db_name}, please double check spelling and capitalization"
end
end
# Add argurment for database, depending on db - change AR::Base below to correct db record
# Find out way to error out if table not found
# use this code to get table field names here, convert to underscore,
# send as aray or array pairs, and pass to template.
# columns = ActiveRecord::Base.connection.columns(table_name)
# field_names = columns.map(&:name)
def create_model_file
template "legacy_model.rb.erb.tt", "app/models/#{@file_folder}/#{file_name}.rb"
end
# kick off another schema dump and rebuild alt db schema
# Will need to persist what tables to run (maybe in db, maybe in alt db schema)
private
def file_name
"#{table_name.underscore}"
end
def class_name
"#{table_name.camelize}"
end
def uddt_type_mapping
{
"uddtBaseKey" => "integer, limit: 8",
"uddtIdPlan" => "string",
"uddtBaseEnum" => "uddt_base_enum",
"uddtBaseShortDesc" => "string",
"uddtBaseLongDesc" => "string",
"uddtBaseContact" => "string",
"uddtAddrCity" => "string",
"uddtAddrState" => "string",
"uddtAddrStreet" => "string",
"uddtAddrZip" => "string",
"uddtContPhone" => "string",
"uddtIdTax" => "string",
"uddtNotesNote" => "string",
"uddtFooter" => "string",
"uddtBaseDate" => "datetime",
"uddtBaseUserId" => "string"
}
end
class LegacyDBModelError < StandardError; end
end
@@ -0,0 +1,23 @@
module <%= @module %>
class <%= class_name %> < <%= @db_record %>
self.table_name = '<%= table_name %>'
<%- @fields.each do |fi| -%>
alias_attribute :<%= fi.underscore.gsub(/(?=\d)/, "_") %>, :<%= fi %>
<%- end -%>
<%- unless db_name == 'BrittonWeb || Baclight' -%>
def attributes
rails_like = {
<%- @fields.each do |fi| -%>
<%= fi.underscore.gsub(/(?=\d)/, "_") %>: self.<%= fi.underscore.gsub(/(?=\d)/, "_") %>,
<%- end -%>
}
super.merge(rails_like)
end
<%- end -%>
end
end
+1 -1
View File
@@ -13,7 +13,7 @@ class JsonWebToken
# encode payload into a jwt
# payload is usually like: { user_id: 1 }
# exp = token expiration (default: 1 hour from now for better security)
# jti = unique token identifier for blacklisting
# jti = unique token identifier for retireing
def self.encode(payload, exp = 1.hour.from_now)
payload[:exp] = exp.to_i
payload[:jti] ||= SecureRandom.uuid
-55
View File
@@ -1,55 +0,0 @@
require "rails_helper"
RSpec.describe BlacklistedToken, type: :model do
let(:user) { User.create!(email: "test@example.com", password: "123456", password_confirmation: "123456") }
describe "validations" do
it "requires jti" do
token = BlacklistedToken.new(user: user, exp: 1.hour.from_now)
expect(token.valid?).to be false
expect(token.errors[:jti]).to include("can't be blank")
end
it "requires exp" do
token = BlacklistedToken.new(user: user, jti: SecureRandom.uuid)
expect(token.valid?).to be false
expect(token.errors[:exp]).to include("can't be blank")
end
it "requires unique jti" do
jti = SecureRandom.uuid
BlacklistedToken.create!(user: user, jti: jti, exp: 1.hour.from_now)
duplicate = BlacklistedToken.new(user: user, jti: jti, exp: 1.hour.from_now)
expect(duplicate.valid?).to be false
expect(duplicate.errors[:jti]).to include("has already been taken")
end
end
describe ".blacklisted?" do
it "returns true for blacklisted tokens" do
jti = SecureRandom.uuid
BlacklistedToken.create!(user: user, jti: jti, exp: 1.hour.from_now)
expect(BlacklistedToken.blacklisted?(jti)).to be true
end
it "returns false for non-blacklisted tokens" do
expect(BlacklistedToken.blacklisted?("non-existent-jti")).to be false
end
end
describe ".cleanup_expired" do
it "removes expired tokens" do
expired_token = BlacklistedToken.create!(user: user, jti: SecureRandom.uuid, exp: 1.day.ago)
valid_token = BlacklistedToken.create!(user: user, jti: SecureRandom.uuid, exp: 1.hour.from_now)
expect {
BlacklistedToken.cleanup_expired
}.to change { BlacklistedToken.count }.by(-1)
expect(BlacklistedToken.exists?(expired_token.id)).to be false
expect(BlacklistedToken.exists?(valid_token.id)).to be true
end
end
end
+55
View File
@@ -0,0 +1,55 @@
require "rails_helper"
RSpec.describe RetiredToken, type: :model do
let(:user) { User.create!(email: "test@example.com", password: "123456", password_confirmation: "123456") }
describe "validations" do
it "requires jti" do
token = RetiredToken.new(user: user, exp: 1.hour.from_now)
expect(token.valid?).to be false
expect(token.errors[:jti]).to include("can't be blank")
end
it "requires exp" do
token = RetiredToken.new(user: user, jti: SecureRandom.uuid)
expect(token.valid?).to be false
expect(token.errors[:exp]).to include("can't be blank")
end
it "requires unique jti" do
jti = SecureRandom.uuid
RetiredToken.create!(user: user, jti: jti, exp: 1.hour.from_now)
duplicate = RetiredToken.new(user: user, jti: jti, exp: 1.hour.from_now)
expect(duplicate.valid?).to be false
expect(duplicate.errors[:jti]).to include("has already been taken")
end
end
describe ".retired?" do
it "returns true for retired tokens" do
jti = SecureRandom.uuid
RetiredToken.create!(user: user, jti: jti, exp: 1.hour.from_now)
expect(RetiredToken.retired?(jti)).to be true
end
it "returns false for non-retired tokens" do
expect(RetiredToken.retired?("non-existent-jti")).to be false
end
end
describe ".cleanup_expired" do
it "removes expired tokens" do
expired_token = RetiredToken.create!(user: user, jti: SecureRandom.uuid, exp: 1.day.ago)
valid_token = RetiredToken.create!(user: user, jti: SecureRandom.uuid, exp: 1.hour.from_now)
expect {
RetiredToken.cleanup_expired
}.to change { RetiredToken.count }.by(-1)
expect(RetiredToken.exists?(expired_token.id)).to be false
expect(RetiredToken.exists?(valid_token.id)).to be true
end
end
end
+6 -6
View File
@@ -2,7 +2,7 @@ require "swagger_helper"
RSpec.describe "api/v1/logout", type: :request do
path "/api/v1/logout" do
post "logs out user and blacklists token" do
post "logs out user and retires token" do
tags "Auth"
consumes "application/json"
produces "application/json"
@@ -20,7 +20,7 @@ RSpec.describe "api/v1/logout", type: :request do
data = JSON.parse(response.body)
expect(data["message"]).to include("Successfully logged out")
# verify token was blacklisted by checking the response
# verify token was retired by checking the response
# (we can't decode the token variable here as it's scoped to the let block)
end
end
@@ -36,16 +36,16 @@ RSpec.describe "api/v1/logout", type: :request do
end
end
describe "blacklisted token rejection" do
it "rejects requests with blacklisted tokens" do
describe "retired token rejection" do
it "rejects requests with retired tokens" do
user = User.create!(email: "test@example.com", password: "123456", password_confirmation: "123456")
token = JsonWebToken.encode(user_id: user.id)
# first logout to blacklist the token
# first logout to retire the token
post "/api/v1/logout", headers: { "Authorization" => "Bearer #{token}" }
expect(response).to have_http_status(:ok)
# try to access protected endpoint with blacklisted token
# try to access protected endpoint with retired token
get "/api/v1/profile", headers: { "Authorization" => "Bearer #{token}" }
expect(response).to have_http_status(:unauthorized)
data = JSON.parse(response.body)
+1 -1
View File
@@ -47,7 +47,7 @@ paths:
example: '123456'
"/api/v1/logout":
post:
summary: logs out user and blacklists token
summary: logs out user and retires token
tags:
- Auth
security: