From 9853d584691a89b6cd4fdddc892bfc7e6df794a2 Mon Sep 17 00:00:00 2001 From: Jason Jordan Date: Thu, 23 Jul 2026 10:41:46 -0400 Subject: [PATCH] Its been a while --- .env.example | 1 - Dockerfile | 6 +- Gemfile | 13 +- Gemfile copy | 99 ++++++++++ Gemfile.lock | 50 +++++- Procfile.dev | 1 + README.md | 16 +- app/controllers/api/v1/auth_controller.rb | 8 +- app/controllers/api/v1/brokers_controller.rb | 16 ++ app/controllers/api/v1/carriers_controller.rb | 8 + app/controllers/api/v1/claims_controller.rb | 18 ++ .../api/v1/employers_controller.rb | 36 ++++ app/controllers/api/v1/id_cards_controller.rb | 96 ++++++++++ app/controllers/api/v1/members_controller.rb | 74 ++++++++ .../api/v1/network_provider_controller.rb | 19 ++ .../api/v1/providers_controller.rb | 8 + app/controllers/application_controller.rb | 1 + app/controllers/concerns/authorize_request.rb | 4 +- .../users/confirmations_controller.rb | 30 ++++ .../users/omniauth_callbacks_controller.rb | 30 ++++ app/controllers/users/passwords_controller.rb | 34 ++++ .../users/registrations_controller.rb | 105 +++++++++++ app/controllers/users/sessions_controller.rb | 49 +++++ app/controllers/users/unlocks_controller.rb | 30 ++++ app/models/application_record.rb | 2 + app/models/baclight/broker.rb | 19 ++ app/models/baclight/carrier.rb | 20 +++ app/models/baclight/employer.rb | 31 ++++ app/models/baclight/id_card_plan.rb | 18 ++ app/models/baclight/id_card_plan_benefit.rb | 17 ++ app/models/baclight/id_card_setup.rb | 25 +++ app/models/baclight/member.rb | 44 +++++ app/models/baclight/provider.rb | 16 ++ app/models/baclight_record.rb | 4 + ...{blacklisted_token.rb => retired_token.rb} | 6 +- app/models/user.rb | 139 ++++++++++++-- app/models/vhcs/vwmb_member.rb | 103 +++++++++++ app/models/vhcs_record.rb | 4 + app/policies/application_policy.rb | 53 ++++++ app/policies/baclight/broker_policy.rb | 13 ++ app/policies/baclight/employer_policy.rb | 15 ++ app/policies/baclight/member_policy.rb | 22 +++ app/policies/id_cards_policy.rb | 18 ++ app/queries/call_stored_proc.rb | 14 ++ app/services/employers_service/id_cards.rb | 41 +++++ app/services/member_claims_service.rb | 33 ++++ app/services/members_service/claims.rb | 41 +++++ app/services/members_service/id_card.rb | 41 +++++ .../members_service/initialize_dashboard.rb | 24 +++ .../members_service/network_provider.rb | 18 ++ app/services/members_service/plan_benefits.rb | 20 +++ bin/brakeman | 0 bin/bundle | 0 bin/dev | 16 ++ bin/docker-entrypoint | 7 + bin/rails | 0 bin/rake | 0 bin/rubocop | 0 bin/setup | 0 config/application.rb | 7 + config/database.yml | 169 ++++++++++-------- config/environments/development.rb | 2 + config/initializers/cors.rb | 5 +- config/initializers/devise.rb | 11 ++ .../initializers/filter_parameter_logging.rb | 2 +- config/routes.rb | 52 +++++- .../20250613174349_devise_create_users.rb | 9 +- ...0260128000001_create_blacklisted_tokens.rb | 14 -- .../20260128000003_add_role_to_users.rb | 6 - .../20260128000001_create_retired_tokens.rb | 14 ++ .../20260128000002_create_refresh_tokens.rb | 0 ...260128000003_add_role_and_name_to_users.rb | 9 + db/schema.rb | 40 +---- development.Dockerfile | 34 ++++ docker-compose.yaml | 31 ++++ lib/generators/legacy_db_model/USAGE | 8 + .../legacy_db_model_generator.rb | 83 +++++++++ .../templates/legacy_model.rb.erb.tt | 23 +++ lib/json_web_token.rb | 2 +- spec/models/blacklisted_token_spec.rb | 55 ------ spec/models/retired_token_spec.rb | 55 ++++++ spec/requests/api/v1/logout_spec.rb | 12 +- swagger/v1/swagger.yaml | 2 +- 83 files changed, 1978 insertions(+), 243 deletions(-) delete mode 100644 .env.example create mode 100644 Gemfile copy create mode 100644 Procfile.dev create mode 100644 app/controllers/api/v1/brokers_controller.rb create mode 100644 app/controllers/api/v1/carriers_controller.rb create mode 100644 app/controllers/api/v1/claims_controller.rb create mode 100644 app/controllers/api/v1/employers_controller.rb create mode 100644 app/controllers/api/v1/id_cards_controller.rb create mode 100644 app/controllers/api/v1/members_controller.rb create mode 100644 app/controllers/api/v1/network_provider_controller.rb create mode 100644 app/controllers/api/v1/providers_controller.rb create mode 100644 app/controllers/users/confirmations_controller.rb create mode 100644 app/controllers/users/omniauth_callbacks_controller.rb create mode 100644 app/controllers/users/passwords_controller.rb create mode 100644 app/controllers/users/registrations_controller.rb create mode 100644 app/controllers/users/sessions_controller.rb create mode 100644 app/controllers/users/unlocks_controller.rb create mode 100644 app/models/baclight/broker.rb create mode 100644 app/models/baclight/carrier.rb create mode 100644 app/models/baclight/employer.rb create mode 100644 app/models/baclight/id_card_plan.rb create mode 100644 app/models/baclight/id_card_plan_benefit.rb create mode 100644 app/models/baclight/id_card_setup.rb create mode 100644 app/models/baclight/member.rb create mode 100644 app/models/baclight/provider.rb create mode 100644 app/models/baclight_record.rb rename app/models/{blacklisted_token.rb => retired_token.rb} (71%) create mode 100644 app/models/vhcs/vwmb_member.rb create mode 100644 app/models/vhcs_record.rb create mode 100644 app/policies/application_policy.rb create mode 100644 app/policies/baclight/broker_policy.rb create mode 100644 app/policies/baclight/employer_policy.rb create mode 100644 app/policies/baclight/member_policy.rb create mode 100644 app/policies/id_cards_policy.rb create mode 100644 app/queries/call_stored_proc.rb create mode 100644 app/services/employers_service/id_cards.rb create mode 100644 app/services/member_claims_service.rb create mode 100644 app/services/members_service/claims.rb create mode 100644 app/services/members_service/id_card.rb create mode 100644 app/services/members_service/initialize_dashboard.rb create mode 100644 app/services/members_service/network_provider.rb create mode 100644 app/services/members_service/plan_benefits.rb mode change 100755 => 100644 bin/brakeman mode change 100755 => 100644 bin/bundle create mode 100644 bin/dev mode change 100755 => 100644 bin/docker-entrypoint mode change 100755 => 100644 bin/rails mode change 100755 => 100644 bin/rake mode change 100755 => 100644 bin/rubocop mode change 100755 => 100644 bin/setup delete mode 100644 db/migrate/20260128000001_create_blacklisted_tokens.rb delete mode 100644 db/migrate/20260128000003_add_role_to_users.rb create mode 100644 db/old/20260128000001_create_retired_tokens.rb rename db/{migrate => old}/20260128000002_create_refresh_tokens.rb (100%) create mode 100644 db/old/20260128000003_add_role_and_name_to_users.rb create mode 100644 development.Dockerfile create mode 100644 docker-compose.yaml create mode 100644 lib/generators/legacy_db_model/USAGE create mode 100644 lib/generators/legacy_db_model/legacy_db_model_generator.rb create mode 100644 lib/generators/legacy_db_model/templates/legacy_model.rb.erb.tt delete mode 100644 spec/models/blacklisted_token_spec.rb create mode 100644 spec/models/retired_token_spec.rb diff --git a/.env.example b/.env.example deleted file mode 100644 index 698461d..0000000 --- a/.env.example +++ /dev/null @@ -1 +0,0 @@ -JWT_SECRET_KEY=your_super_secret_key_here diff --git a/Dockerfile b/Dockerfile index dc288c1..c1f1260 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Gemfile b/Gemfile index 375d402..82e2979 100644 --- a/Gemfile +++ b/Gemfile @@ -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' diff --git a/Gemfile copy b/Gemfile copy new file mode 100644 index 0000000..70b345d --- /dev/null +++ b/Gemfile copy @@ -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' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 6f117f2..a3744ee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..c2cb467 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1 @@ +app: bin/rails server -b 0.0.0.0 -p 3005 diff --git a/README.md b/README.md index f3a2921..957c351 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ POST /api/v1/logout Authorization: Bearer ``` -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 diff --git a/app/controllers/api/v1/auth_controller.rb b/app/controllers/api/v1/auth_controller.rb index 5048aaa..e365f49 100644 --- a/app/controllers/api/v1/auth_controller.rb +++ b/app/controllers/api/v1/auth_controller.rb @@ -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 diff --git a/app/controllers/api/v1/brokers_controller.rb b/app/controllers/api/v1/brokers_controller.rb new file mode 100644 index 0000000..0bed621 --- /dev/null +++ b/app/controllers/api/v1/brokers_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/api/v1/carriers_controller.rb b/app/controllers/api/v1/carriers_controller.rb new file mode 100644 index 0000000..bb810a7 --- /dev/null +++ b/app/controllers/api/v1/carriers_controller.rb @@ -0,0 +1,8 @@ +module Api + module V1 + class CarriersController < ApplicationController + before_action :authenticate_user! + + end + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/claims_controller.rb b/app/controllers/api/v1/claims_controller.rb new file mode 100644 index 0000000..cc68222 --- /dev/null +++ b/app/controllers/api/v1/claims_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/api/v1/employers_controller.rb b/app/controllers/api/v1/employers_controller.rb new file mode 100644 index 0000000..67a9e76 --- /dev/null +++ b/app/controllers/api/v1/employers_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/api/v1/id_cards_controller.rb b/app/controllers/api/v1/id_cards_controller.rb new file mode 100644 index 0000000..0d31093 --- /dev/null +++ b/app/controllers/api/v1/id_cards_controller.rb @@ -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 diff --git a/app/controllers/api/v1/members_controller.rb b/app/controllers/api/v1/members_controller.rb new file mode 100644 index 0000000..1954cce --- /dev/null +++ b/app/controllers/api/v1/members_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/api/v1/network_provider_controller.rb b/app/controllers/api/v1/network_provider_controller.rb new file mode 100644 index 0000000..902772b --- /dev/null +++ b/app/controllers/api/v1/network_provider_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/api/v1/providers_controller.rb b/app/controllers/api/v1/providers_controller.rb new file mode 100644 index 0000000..7950501 --- /dev/null +++ b/app/controllers/api/v1/providers_controller.rb @@ -0,0 +1,8 @@ +module Api + module V1 + class ProvidersController < ApplicationController + before_action :authenticate_user! + + end + end +end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4ac8823..e32cbbb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,3 @@ class ApplicationController < ActionController::API + include Pundit::Authorization end diff --git a/app/controllers/concerns/authorize_request.rb b/app/controllers/concerns/authorize_request.rb index 0ecae68..d56a2ce 100644 --- a/app/controllers/concerns/authorize_request.rb +++ b/app/controllers/concerns/authorize_request.rb @@ -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 diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb new file mode 100644 index 0000000..fa535c0 --- /dev/null +++ b/app/controllers/users/confirmations_controller.rb @@ -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 diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 0000000..593f547 --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -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 diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb new file mode 100644 index 0000000..259dbb0 --- /dev/null +++ b/app/controllers/users/passwords_controller.rb @@ -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 diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb new file mode 100644 index 0000000..1245c65 --- /dev/null +++ b/app/controllers/users/registrations_controller.rb @@ -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 diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb new file mode 100644 index 0000000..50c34d6 --- /dev/null +++ b/app/controllers/users/sessions_controller.rb @@ -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 diff --git a/app/controllers/users/unlocks_controller.rb b/app/controllers/users/unlocks_controller.rb new file mode 100644 index 0000000..2c410dc --- /dev/null +++ b/app/controllers/users/unlocks_controller.rb @@ -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 diff --git a/app/models/application_record.rb b/app/models/application_record.rb index b63caeb..d1eebe9 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,5 @@ class ApplicationRecord < ActiveRecord::Base primary_abstract_class + + establish_connection :primary end diff --git a/app/models/baclight/broker.rb b/app/models/baclight/broker.rb new file mode 100644 index 0000000..9e8ffdc --- /dev/null +++ b/app/models/baclight/broker.rb @@ -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 \ No newline at end of file diff --git a/app/models/baclight/carrier.rb b/app/models/baclight/carrier.rb new file mode 100644 index 0000000..7290fc4 --- /dev/null +++ b/app/models/baclight/carrier.rb @@ -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 \ No newline at end of file diff --git a/app/models/baclight/employer.rb b/app/models/baclight/employer.rb new file mode 100644 index 0000000..47f33c3 --- /dev/null +++ b/app/models/baclight/employer.rb @@ -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 \ No newline at end of file diff --git a/app/models/baclight/id_card_plan.rb b/app/models/baclight/id_card_plan.rb new file mode 100644 index 0000000..9b73676 --- /dev/null +++ b/app/models/baclight/id_card_plan.rb @@ -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 \ No newline at end of file diff --git a/app/models/baclight/id_card_plan_benefit.rb b/app/models/baclight/id_card_plan_benefit.rb new file mode 100644 index 0000000..4a7ec87 --- /dev/null +++ b/app/models/baclight/id_card_plan_benefit.rb @@ -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 \ No newline at end of file diff --git a/app/models/baclight/id_card_setup.rb b/app/models/baclight/id_card_setup.rb new file mode 100644 index 0000000..68c59fb --- /dev/null +++ b/app/models/baclight/id_card_setup.rb @@ -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 \ No newline at end of file diff --git a/app/models/baclight/member.rb b/app/models/baclight/member.rb new file mode 100644 index 0000000..8d0e0be --- /dev/null +++ b/app/models/baclight/member.rb @@ -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 \ No newline at end of file diff --git a/app/models/baclight/provider.rb b/app/models/baclight/provider.rb new file mode 100644 index 0000000..c2b18b7 --- /dev/null +++ b/app/models/baclight/provider.rb @@ -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 \ No newline at end of file diff --git a/app/models/baclight_record.rb b/app/models/baclight_record.rb new file mode 100644 index 0000000..39428fd --- /dev/null +++ b/app/models/baclight_record.rb @@ -0,0 +1,4 @@ +class BaclightRecord < ActiveRecord::Base + self.abstract_class = true + connects_to database: { writing: :baclight, reading: :baclight } +end \ No newline at end of file diff --git a/app/models/blacklisted_token.rb b/app/models/retired_token.rb similarity index 71% rename from app/models/blacklisted_token.rb rename to app/models/retired_token.rb index 89b8944..2a53a37 100644 --- a/app/models/blacklisted_token.rb +++ b/app/models/retired_token.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index b7fba6a..2f2415f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,17 +1,132 @@ class User < ApplicationRecord - # Include default devise modules. Others available are: - # :recoverable, :rememberable, :confirmable, :lockable, :timeoutable, :trackable and :omniauthable - devise :database_authenticatable, :registerable, - :validatable + include Devise::JWT::RevocationStrategies::JTIMatcher - # associations - has_many :blacklisted_tokens, dependent: :destroy - has_many :refresh_tokens, dependent: :destroy + 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 - # role-based authorization - enum :role, { user: 0, admin: 1, moderator: 2 } + # role-based authorization + 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 - # 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? } end diff --git a/app/models/vhcs/vwmb_member.rb b/app/models/vhcs/vwmb_member.rb new file mode 100644 index 0000000..27deb68 --- /dev/null +++ b/app/models/vhcs/vwmb_member.rb @@ -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 \ No newline at end of file diff --git a/app/models/vhcs_record.rb b/app/models/vhcs_record.rb new file mode 100644 index 0000000..f9b8f74 --- /dev/null +++ b/app/models/vhcs_record.rb @@ -0,0 +1,4 @@ +class VhcsRecord < ActiveRecord::Base + self.abstract_class = true + connects_to database: { writing: :vhcs, reading: :vhcs } +end \ No newline at end of file diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb new file mode 100644 index 0000000..be644fe --- /dev/null +++ b/app/policies/application_policy.rb @@ -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 diff --git a/app/policies/baclight/broker_policy.rb b/app/policies/baclight/broker_policy.rb new file mode 100644 index 0000000..9c7d56e --- /dev/null +++ b/app/policies/baclight/broker_policy.rb @@ -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 \ No newline at end of file diff --git a/app/policies/baclight/employer_policy.rb b/app/policies/baclight/employer_policy.rb new file mode 100644 index 0000000..4723141 --- /dev/null +++ b/app/policies/baclight/employer_policy.rb @@ -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 \ No newline at end of file diff --git a/app/policies/baclight/member_policy.rb b/app/policies/baclight/member_policy.rb new file mode 100644 index 0000000..3e19828 --- /dev/null +++ b/app/policies/baclight/member_policy.rb @@ -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 \ No newline at end of file diff --git a/app/policies/id_cards_policy.rb b/app/policies/id_cards_policy.rb new file mode 100644 index 0000000..4cf802f --- /dev/null +++ b/app/policies/id_cards_policy.rb @@ -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 diff --git a/app/queries/call_stored_proc.rb b/app/queries/call_stored_proc.rb new file mode 100644 index 0000000..5a8bb49 --- /dev/null +++ b/app/queries/call_stored_proc.rb @@ -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 diff --git a/app/services/employers_service/id_cards.rb b/app/services/employers_service/id_cards.rb new file mode 100644 index 0000000..8bae65d --- /dev/null +++ b/app/services/employers_service/id_cards.rb @@ -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 \ No newline at end of file diff --git a/app/services/member_claims_service.rb b/app/services/member_claims_service.rb new file mode 100644 index 0000000..b27c09f --- /dev/null +++ b/app/services/member_claims_service.rb @@ -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) \ No newline at end of file diff --git a/app/services/members_service/claims.rb b/app/services/members_service/claims.rb new file mode 100644 index 0000000..0ba71b9 --- /dev/null +++ b/app/services/members_service/claims.rb @@ -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) \ No newline at end of file diff --git a/app/services/members_service/id_card.rb b/app/services/members_service/id_card.rb new file mode 100644 index 0000000..b098d08 --- /dev/null +++ b/app/services/members_service/id_card.rb @@ -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 diff --git a/app/services/members_service/initialize_dashboard.rb b/app/services/members_service/initialize_dashboard.rb new file mode 100644 index 0000000..09beda8 --- /dev/null +++ b/app/services/members_service/initialize_dashboard.rb @@ -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 \ No newline at end of file diff --git a/app/services/members_service/network_provider.rb b/app/services/members_service/network_provider.rb new file mode 100644 index 0000000..d51579b --- /dev/null +++ b/app/services/members_service/network_provider.rb @@ -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 diff --git a/app/services/members_service/plan_benefits.rb b/app/services/members_service/plan_benefits.rb new file mode 100644 index 0000000..b0ed8fb --- /dev/null +++ b/app/services/members_service/plan_benefits.rb @@ -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 diff --git a/bin/brakeman b/bin/brakeman old mode 100755 new mode 100644 diff --git a/bin/bundle b/bin/bundle old mode 100755 new mode 100644 diff --git a/bin/dev b/bin/dev new file mode 100644 index 0000000..24e34ff --- /dev/null +++ b/bin/dev @@ -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 "$@" diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint old mode 100755 new mode 100644 index 840d093..d1fa779 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -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 "${@}" diff --git a/bin/rails b/bin/rails old mode 100755 new mode 100644 diff --git a/bin/rake b/bin/rake old mode 100755 new mode 100644 diff --git a/bin/rubocop b/bin/rubocop old mode 100755 new mode 100644 diff --git a/bin/setup b/bin/setup old mode 100755 new mode 100644 diff --git a/config/application.rb b/config/application.rb index 1eb0548..9bf9054 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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 diff --git a/config/database.yml b/config/database.yml index 32fab38..f353bd2 100644 --- a/config/database.yml +++ b/config/database.yml @@ -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 - pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> +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 diff --git a/config/environments/development.rb b/config/environments/development.rb index 98128ff..246a2a3 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -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? diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index 835d425..e68a72b 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -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 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 574afd8..bee8b06 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -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' diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index c010b83..ad0112d 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -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 ] diff --git a/config/routes.rb b/config/routes.rb index f9661e5..dc2e1f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20250613174349_devise_create_users.rb b/db/migrate/20250613174349_devise_create_users.rb index 43f526a..1e8f58d 100644 --- a/db/migrate/20250613174349_devise_create_users.rb +++ b/db/migrate/20250613174349_devise_create_users.rb @@ -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 diff --git a/db/migrate/20260128000001_create_blacklisted_tokens.rb b/db/migrate/20260128000001_create_blacklisted_tokens.rb deleted file mode 100644 index 13c0b2e..0000000 --- a/db/migrate/20260128000001_create_blacklisted_tokens.rb +++ /dev/null @@ -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 diff --git a/db/migrate/20260128000003_add_role_to_users.rb b/db/migrate/20260128000003_add_role_to_users.rb deleted file mode 100644 index 017b0d9..0000000 --- a/db/migrate/20260128000003_add_role_to_users.rb +++ /dev/null @@ -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 diff --git a/db/old/20260128000001_create_retired_tokens.rb b/db/old/20260128000001_create_retired_tokens.rb new file mode 100644 index 0000000..aee5fd7 --- /dev/null +++ b/db/old/20260128000001_create_retired_tokens.rb @@ -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 diff --git a/db/migrate/20260128000002_create_refresh_tokens.rb b/db/old/20260128000002_create_refresh_tokens.rb similarity index 100% rename from db/migrate/20260128000002_create_refresh_tokens.rb rename to db/old/20260128000002_create_refresh_tokens.rb diff --git a/db/old/20260128000003_add_role_and_name_to_users.rb b/db/old/20260128000003_add_role_and_name_to_users.rb new file mode 100644 index 0000000..5d97674 --- /dev/null +++ b/db/old/20260128000003_add_role_and_name_to_users.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 66a9129..0b3b003 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/development.Dockerfile b/development.Dockerfile new file mode 100644 index 0000000..8f91777 --- /dev/null +++ b/development.Dockerfile @@ -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"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..3260bbc --- /dev/null +++ b/docker-compose.yaml @@ -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: diff --git a/lib/generators/legacy_db_model/USAGE b/lib/generators/legacy_db_model/USAGE new file mode 100644 index 0000000..6857db4 --- /dev/null +++ b/lib/generators/legacy_db_model/USAGE @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + bin/rails generate legacy_db_model Thing + + This will create: + what/will/it/create diff --git a/lib/generators/legacy_db_model/legacy_db_model_generator.rb b/lib/generators/legacy_db_model/legacy_db_model_generator.rb new file mode 100644 index 0000000..c14e306 --- /dev/null +++ b/lib/generators/legacy_db_model/legacy_db_model_generator.rb @@ -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 diff --git a/lib/generators/legacy_db_model/templates/legacy_model.rb.erb.tt b/lib/generators/legacy_db_model/templates/legacy_model.rb.erb.tt new file mode 100644 index 0000000..6f2b0ec --- /dev/null +++ b/lib/generators/legacy_db_model/templates/legacy_model.rb.erb.tt @@ -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 \ No newline at end of file diff --git a/lib/json_web_token.rb b/lib/json_web_token.rb index ff329ff..cda7b35 100644 --- a/lib/json_web_token.rb +++ b/lib/json_web_token.rb @@ -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 diff --git a/spec/models/blacklisted_token_spec.rb b/spec/models/blacklisted_token_spec.rb deleted file mode 100644 index b516d0f..0000000 --- a/spec/models/blacklisted_token_spec.rb +++ /dev/null @@ -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 diff --git a/spec/models/retired_token_spec.rb b/spec/models/retired_token_spec.rb new file mode 100644 index 0000000..e989a52 --- /dev/null +++ b/spec/models/retired_token_spec.rb @@ -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 diff --git a/spec/requests/api/v1/logout_spec.rb b/spec/requests/api/v1/logout_spec.rb index aaa73db..ceea95f 100644 --- a/spec/requests/api/v1/logout_spec.rb +++ b/spec/requests/api/v1/logout_spec.rb @@ -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) diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 25bf820..781f0c2 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -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: