From c2854fa0f71adc21352910b8a9d5f5767679480e Mon Sep 17 00:00:00 2001 From: Rocket Date: Mon, 1 Apr 2024 14:35:40 -0700 Subject: [PATCH 01/76] Run rails new with flags --database=postgresql --css=sass --name=TemplateApplicationRails --- app/.dockerignore | 37 ++ app/.gitattributes | 9 + app/.gitignore | 38 +++ app/.ruby-version | 1 + app/Dockerfile | 62 ++++ app/Gemfile | 70 ++++ app/Gemfile.lock | 316 ++++++++++++++++++ app/Procfile.dev | 2 + app/README.md | 24 ++ app/Rakefile | 6 + app/app/assets/builds/.keep | 0 app/app/assets/config/manifest.js | 4 + app/app/assets/images/.keep | 0 app/app/assets/stylesheets/application.css | 15 + app/app/assets/stylesheets/application.scss | 1 + app/app/channels/application_cable/channel.rb | 4 + .../channels/application_cable/connection.rb | 4 + app/app/controllers/application_controller.rb | 2 + app/app/controllers/concerns/.keep | 0 app/app/helpers/application_helper.rb | 2 + app/app/javascript/application.js | 3 + app/app/javascript/controllers/application.js | 9 + .../controllers/hello_controller.js | 7 + app/app/javascript/controllers/index.js | 11 + app/app/jobs/application_job.rb | 7 + app/app/mailers/application_mailer.rb | 4 + app/app/models/application_record.rb | 3 + app/app/models/concerns/.keep | 0 app/app/views/layouts/application.html.erb | 16 + app/app/views/layouts/mailer.html.erb | 13 + app/app/views/layouts/mailer.text.erb | 1 + app/bin/bundle | 109 ++++++ app/bin/dev | 8 + app/bin/docker-entrypoint | 8 + app/bin/importmap | 4 + app/bin/rails | 4 + app/bin/rake | 4 + app/bin/setup | 33 ++ app/config.ru | 6 + app/config/application.rb | 27 ++ app/config/boot.rb | 4 + app/config/cable.yml | 10 + app/config/credentials.yml.enc | 1 + app/config/database.yml | 84 +++++ app/config/environment.rb | 5 + app/config/environments/development.rb | 76 +++++ app/config/environments/production.rb | 97 ++++++ app/config/environments/test.rb | 64 ++++ app/config/importmap.rb | 7 + app/config/initializers/assets.rb | 12 + .../initializers/content_security_policy.rb | 25 ++ .../initializers/filter_parameter_logging.rb | 8 + app/config/initializers/inflections.rb | 16 + app/config/initializers/permissions_policy.rb | 13 + app/config/locales/en.yml | 31 ++ app/config/puma.rb | 35 ++ app/config/routes.rb | 10 + app/config/storage.yml | 34 ++ app/db/seeds.rb | 9 + app/lib/assets/.keep | 0 app/lib/tasks/.keep | 0 app/log/.keep | 0 app/public/404.html | 67 ++++ app/public/422.html | 67 ++++ app/public/500.html | 66 ++++ app/public/apple-touch-icon-precomposed.png | 0 app/public/apple-touch-icon.png | 0 app/public/favicon.ico | 0 app/public/robots.txt | 1 + app/storage/.keep | 0 app/tmp/.keep | 0 app/tmp/pids/.keep | 0 app/tmp/storage/.keep | 0 app/vendor/.keep | 0 app/vendor/javascript/.keep | 0 75 files changed, 1616 insertions(+) create mode 100644 app/.dockerignore create mode 100644 app/.gitattributes create mode 100644 app/.gitignore create mode 100644 app/.ruby-version create mode 100644 app/Dockerfile create mode 100644 app/Gemfile create mode 100644 app/Gemfile.lock create mode 100644 app/Procfile.dev create mode 100644 app/README.md create mode 100644 app/Rakefile create mode 100644 app/app/assets/builds/.keep create mode 100644 app/app/assets/config/manifest.js create mode 100644 app/app/assets/images/.keep create mode 100644 app/app/assets/stylesheets/application.css create mode 100644 app/app/assets/stylesheets/application.scss create mode 100644 app/app/channels/application_cable/channel.rb create mode 100644 app/app/channels/application_cable/connection.rb create mode 100644 app/app/controllers/application_controller.rb create mode 100644 app/app/controllers/concerns/.keep create mode 100644 app/app/helpers/application_helper.rb create mode 100644 app/app/javascript/application.js create mode 100644 app/app/javascript/controllers/application.js create mode 100644 app/app/javascript/controllers/hello_controller.js create mode 100644 app/app/javascript/controllers/index.js create mode 100644 app/app/jobs/application_job.rb create mode 100644 app/app/mailers/application_mailer.rb create mode 100644 app/app/models/application_record.rb create mode 100644 app/app/models/concerns/.keep create mode 100644 app/app/views/layouts/application.html.erb create mode 100644 app/app/views/layouts/mailer.html.erb create mode 100644 app/app/views/layouts/mailer.text.erb create mode 100755 app/bin/bundle create mode 100755 app/bin/dev create mode 100755 app/bin/docker-entrypoint create mode 100755 app/bin/importmap create mode 100755 app/bin/rails create mode 100755 app/bin/rake create mode 100755 app/bin/setup create mode 100644 app/config.ru create mode 100644 app/config/application.rb create mode 100644 app/config/boot.rb create mode 100644 app/config/cable.yml create mode 100644 app/config/credentials.yml.enc create mode 100644 app/config/database.yml create mode 100644 app/config/environment.rb create mode 100644 app/config/environments/development.rb create mode 100644 app/config/environments/production.rb create mode 100644 app/config/environments/test.rb create mode 100644 app/config/importmap.rb create mode 100644 app/config/initializers/assets.rb create mode 100644 app/config/initializers/content_security_policy.rb create mode 100644 app/config/initializers/filter_parameter_logging.rb create mode 100644 app/config/initializers/inflections.rb create mode 100644 app/config/initializers/permissions_policy.rb create mode 100644 app/config/locales/en.yml create mode 100644 app/config/puma.rb create mode 100644 app/config/routes.rb create mode 100644 app/config/storage.yml create mode 100644 app/db/seeds.rb create mode 100644 app/lib/assets/.keep create mode 100644 app/lib/tasks/.keep create mode 100644 app/log/.keep create mode 100644 app/public/404.html create mode 100644 app/public/422.html create mode 100644 app/public/500.html create mode 100644 app/public/apple-touch-icon-precomposed.png create mode 100644 app/public/apple-touch-icon.png create mode 100644 app/public/favicon.ico create mode 100644 app/public/robots.txt create mode 100644 app/storage/.keep create mode 100644 app/tmp/.keep create mode 100644 app/tmp/pids/.keep create mode 100644 app/tmp/storage/.keep create mode 100644 app/vendor/.keep create mode 100644 app/vendor/javascript/.keep diff --git a/app/.dockerignore b/app/.dockerignore new file mode 100644 index 0000000..9612375 --- /dev/null +++ b/app/.dockerignore @@ -0,0 +1,37 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets diff --git a/app/.gitattributes b/app/.gitattributes new file mode 100644 index 0000000..8dc4323 --- /dev/null +++ b/app/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..9b66b15 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/app/assets/builds/* +!/app/assets/builds/.keep diff --git a/app/.ruby-version b/app/.ruby-version new file mode 100644 index 0000000..15a2799 --- /dev/null +++ b/app/.ruby-version @@ -0,0 +1 @@ +3.3.0 diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..2c1e8b4 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,62 @@ +# syntax = docker/dockerfile:1 + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile +ARG RUBY_VERSION=3.3.0 +FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base + +# Rails app lives here +WORKDIR /rails + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + + +# Throw-away build stage to reduce size of final image +FROM base as build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + +# Final stage for app image +FROM base + +# Install packages needed for deployment +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libvips postgresql-client && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Copy built artifacts: gems, application +COPY --from=build /usr/local/bundle /usr/local/bundle +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN useradd rails --create-home --shell /bin/bash && \ + chown -R rails:rails db log storage tmp +USER rails:rails + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD ["./bin/rails", "server"] diff --git a/app/Gemfile b/app/Gemfile new file mode 100644 index 0000000..f4015d2 --- /dev/null +++ b/app/Gemfile @@ -0,0 +1,70 @@ +source "https://rubygems.org" + +ruby "3.3.0" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 7.1.3", ">= 7.1.3.2" + +# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] +gem "sprockets-rails" + +# 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" + +# 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" + +# Use Dart SASS [https://github.com/rails/dartsass-rails] +gem "dartsass-rails" + +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Redis adapter to run Action Cable in production +# gem "redis", ">= 4.0.1" + +# 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[ windows 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 ] +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" + + # Speed up commands on slow machines / big apps [https://github.com/rails/spring] + # gem "spring" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end diff --git a/app/Gemfile.lock b/app/Gemfile.lock new file mode 100644 index 0000000..1868a3d --- /dev/null +++ b/app/Gemfile.lock @@ -0,0 +1,316 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.1.3.2) + actionpack (= 7.1.3.2) + activesupport (= 7.1.3.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.1.3.2) + actionpack (= 7.1.3.2) + activejob (= 7.1.3.2) + activerecord (= 7.1.3.2) + activestorage (= 7.1.3.2) + activesupport (= 7.1.3.2) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.3.2) + actionpack (= 7.1.3.2) + actionview (= 7.1.3.2) + activejob (= 7.1.3.2) + activesupport (= 7.1.3.2) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.3.2) + actionview (= 7.1.3.2) + activesupport (= 7.1.3.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.3.2) + actionpack (= 7.1.3.2) + activerecord (= 7.1.3.2) + activestorage (= 7.1.3.2) + activesupport (= 7.1.3.2) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.1.3.2) + activesupport (= 7.1.3.2) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.3.2) + activesupport (= 7.1.3.2) + globalid (>= 0.3.6) + activemodel (7.1.3.2) + activesupport (= 7.1.3.2) + activerecord (7.1.3.2) + activemodel (= 7.1.3.2) + activesupport (= 7.1.3.2) + timeout (>= 0.4.0) + activestorage (7.1.3.2) + actionpack (= 7.1.3.2) + activejob (= 7.1.3.2) + activerecord (= 7.1.3.2) + activesupport (= 7.1.3.2) + marcel (~> 1.0) + activesupport (7.1.3.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + base64 (0.2.0) + bigdecimal (3.1.7) + bindex (0.8.1) + bootsnap (1.18.3) + msgpack (~> 1.2) + builder (3.2.4) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + concurrent-ruby (1.2.3) + connection_pool (2.4.1) + crass (1.0.6) + dartsass-rails (0.5.0) + railties (>= 6.0.0) + sass-embedded (~> 1.63) + date (3.3.4) + debug (1.9.2) + irb (~> 1.10) + reline (>= 0.3.8) + drb (2.2.1) + erubi (1.12.0) + globalid (1.2.1) + activesupport (>= 6.1) + google-protobuf (4.26.1) + rake (>= 13) + google-protobuf (4.26.1-aarch64-linux) + rake (>= 13) + google-protobuf (4.26.1-arm64-darwin) + rake (>= 13) + google-protobuf (4.26.1-x86-linux) + rake (>= 13) + google-protobuf (4.26.1-x86_64-darwin) + rake (>= 13) + google-protobuf (4.26.1-x86_64-linux) + rake (>= 13) + i18n (1.14.4) + concurrent-ruby (~> 1.0) + importmap-rails (2.0.1) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.7.2) + irb (1.12.0) + rdoc + reline (>= 0.4.2) + jbuilder (2.11.5) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + mini_mime (1.1.5) + minitest (5.22.3) + msgpack (1.7.2) + mutex_m (0.2.0) + net-imap (0.4.10) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.1) + nokogiri (1.16.3-aarch64-linux) + racc (~> 1.4) + nokogiri (1.16.3-arm-linux) + racc (~> 1.4) + nokogiri (1.16.3-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.3-x86-linux) + racc (~> 1.4) + nokogiri (1.16.3-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.3-x86_64-linux) + racc (~> 1.4) + pg (1.5.6) + psych (5.1.2) + stringio + public_suffix (5.0.4) + puma (6.4.2) + nio4r (~> 2.0) + racc (1.7.3) + rack (3.0.10) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (7.1.3.2) + actioncable (= 7.1.3.2) + actionmailbox (= 7.1.3.2) + actionmailer (= 7.1.3.2) + actionpack (= 7.1.3.2) + actiontext (= 7.1.3.2) + actionview (= 7.1.3.2) + activejob (= 7.1.3.2) + activemodel (= 7.1.3.2) + activerecord (= 7.1.3.2) + activestorage (= 7.1.3.2) + activesupport (= 7.1.3.2) + bundler (>= 1.15.0) + railties (= 7.1.3.2) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.3.2) + actionpack (= 7.1.3.2) + activesupport (= 7.1.3.2) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rake (13.1.0) + rdoc (6.6.3.1) + psych (>= 4.0.0) + regexp_parser (2.9.0) + reline (0.5.0) + io-console (~> 0.5) + rexml (3.2.6) + rubyzip (2.3.2) + sass-embedded (1.72.0-aarch64-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + sass-embedded (1.72.0-aarch64-linux-musl) + google-protobuf (>= 3.25, < 5.0) + sass-embedded (1.72.0-arm-linux-gnueabihf) + google-protobuf (>= 3.25, < 5.0) + sass-embedded (1.72.0-arm-linux-musleabihf) + google-protobuf (>= 3.25, < 5.0) + sass-embedded (1.72.0-arm64-darwin) + google-protobuf (>= 3.25, < 5.0) + sass-embedded (1.72.0-x86-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + sass-embedded (1.72.0-x86-linux-musl) + google-protobuf (>= 3.25, < 5.0) + sass-embedded (1.72.0-x86_64-darwin) + google-protobuf (>= 3.25, < 5.0) + sass-embedded (1.72.0-x86_64-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + sass-embedded (1.72.0-x86_64-linux-musl) + google-protobuf (>= 3.25, < 5.0) + selenium-webdriver (4.19.0) + base64 (~> 0.2) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + sprockets (4.2.1) + concurrent-ruby (~> 1.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + stimulus-rails (1.3.3) + railties (>= 6.0.0) + stringio (3.1.0) + thor (1.3.1) + timeout (0.4.1) + turbo-rails (2.0.5) + actionpack (>= 6.0.0) + activejob (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webrick (1.8.1) + websocket (1.2.10) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.6.13) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux + arm-linux-gnueabihf + arm-linux-musleabihf + arm64-darwin + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bootsnap + capybara + dartsass-rails + debug + importmap-rails + jbuilder + pg (~> 1.1) + puma (>= 5.0) + rails (~> 7.1.3, >= 7.1.3.2) + selenium-webdriver + sprockets-rails + stimulus-rails + turbo-rails + tzinfo-data + web-console + +RUBY VERSION + ruby 3.3.0p0 + +BUNDLED WITH + 2.5.6 diff --git a/app/Procfile.dev b/app/Procfile.dev new file mode 100644 index 0000000..852e6c7 --- /dev/null +++ b/app/Procfile.dev @@ -0,0 +1,2 @@ +web: bin/rails server -p 3000 +css: bin/rails dartsass:watch diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..7db80e4 --- /dev/null +++ b/app/README.md @@ -0,0 +1,24 @@ +# README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... diff --git a/app/Rakefile b/app/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/app/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/app/assets/builds/.keep b/app/app/assets/builds/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/app/assets/config/manifest.js b/app/app/assets/config/manifest.js new file mode 100644 index 0000000..4028c22 --- /dev/null +++ b/app/app/assets/config/manifest.js @@ -0,0 +1,4 @@ +//= link_tree ../images +//= link_tree ../../javascript .js +//= link_tree ../../../vendor/javascript .js +//= link_tree ../builds diff --git a/app/app/assets/images/.keep b/app/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/app/assets/stylesheets/application.css b/app/app/assets/stylesheets/application.css new file mode 100644 index 0000000..288b9ab --- /dev/null +++ b/app/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/app/app/assets/stylesheets/application.scss b/app/app/assets/stylesheets/application.scss new file mode 100644 index 0000000..8d7af90 --- /dev/null +++ b/app/app/assets/stylesheets/application.scss @@ -0,0 +1 @@ +// Sassy diff --git a/app/app/channels/application_cable/channel.rb b/app/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/app/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/app/channels/application_cable/connection.rb b/app/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/app/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/app/controllers/application_controller.rb b/app/app/controllers/application_controller.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/app/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/app/app/controllers/concerns/.keep b/app/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/app/helpers/application_helper.rb b/app/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/app/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/app/javascript/application.js b/app/app/javascript/application.js new file mode 100644 index 0000000..0d7b494 --- /dev/null +++ b/app/app/javascript/application.js @@ -0,0 +1,3 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" diff --git a/app/app/javascript/controllers/application.js b/app/app/javascript/controllers/application.js new file mode 100644 index 0000000..1213e85 --- /dev/null +++ b/app/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/app/javascript/controllers/hello_controller.js b/app/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000..5975c07 --- /dev/null +++ b/app/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.element.textContent = "Hello World!" + } +} diff --git a/app/app/javascript/controllers/index.js b/app/app/javascript/controllers/index.js new file mode 100644 index 0000000..54ad4ca --- /dev/null +++ b/app/app/javascript/controllers/index.js @@ -0,0 +1,11 @@ +// Import and register all your controllers from the importmap under controllers/* + +import { application } from "controllers/application" + +// Eager load all controllers defined in the import map under controllers/**/*_controller +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) + +// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) +// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" +// lazyLoadControllersFrom("controllers", application) diff --git a/app/app/jobs/application_job.rb b/app/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/app/mailers/application_mailer.rb b/app/app/mailers/application_mailer.rb new file mode 100644 index 0000000..3c34c81 --- /dev/null +++ b/app/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/app/models/application_record.rb b/app/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/app/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/app/models/concerns/.keep b/app/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/app/views/layouts/application.html.erb b/app/app/views/layouts/application.html.erb new file mode 100644 index 0000000..a80e872 --- /dev/null +++ b/app/app/views/layouts/application.html.erb @@ -0,0 +1,16 @@ + + + + TemplateApplicationRails + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + <%= yield %> + + diff --git a/app/app/views/layouts/mailer.html.erb b/app/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..3aac900 --- /dev/null +++ b/app/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/app/views/layouts/mailer.text.erb b/app/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/bin/bundle b/app/bin/bundle new file mode 100755 index 0000000..50da5fd --- /dev/null +++ b/app/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/app/bin/dev b/app/bin/dev new file mode 100755 index 0000000..74ade16 --- /dev/null +++ b/app/bin/dev @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman +fi + +exec foreman start -f Procfile.dev "$@" diff --git a/app/bin/docker-entrypoint b/app/bin/docker-entrypoint new file mode 100755 index 0000000..67ef493 --- /dev/null +++ b/app/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# If running the rails server then create or migrate existing database +if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/app/bin/importmap b/app/bin/importmap new file mode 100755 index 0000000..36502ab --- /dev/null +++ b/app/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/app/bin/rails b/app/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/app/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/app/bin/rake b/app/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/app/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/app/bin/setup b/app/bin/setup new file mode 100755 index 0000000..3cd5a9d --- /dev/null +++ b/app/bin/setup @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + puts "\n== Restarting application server ==" + system! "bin/rails restart" +end diff --git a/app/config.ru b/app/config.ru new file mode 100644 index 0000000..4a3c09a --- /dev/null +++ b/app/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/app/config/application.rb b/app/config/application.rb new file mode 100644 index 0000000..5448663 --- /dev/null +++ b/app/config/application.rb @@ -0,0 +1,27 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module TemplateApplicationRails + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 7.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w(assets tasks)) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/app/config/boot.rb b/app/config/boot.rb new file mode 100644 index 0000000..988a5dd --- /dev/null +++ b/app/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/app/config/cable.yml b/app/config/cable.yml new file mode 100644 index 0000000..8979b1b --- /dev/null +++ b/app/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: template_application_rails_production diff --git a/app/config/credentials.yml.enc b/app/config/credentials.yml.enc new file mode 100644 index 0000000..8e564d2 --- /dev/null +++ b/app/config/credentials.yml.enc @@ -0,0 +1 @@ +r/2Qv1Rt0kJvM3tfeP83+2j3lOinPYglSkHRNWnrFo0q8uAKkZbB2Zhnmw4UWkXzyqnzH3EjB7VhLqtdA3j1qLQaaTCfGw7rKpgXTlniegrigJoztLM6GlJH+jNfPFl9TgrPgHwJ+OHsgNGhRqi8uStAMi+onIuIqihwkxP4N1oQnRV2ywM4wmS+JPDPcZWX7SObIiEGFm6nmWKUYFDUdAeOVVa2dfhKOD4qu4jMUeopfZbF2dsy4OknEo5kw6Qc479GSlUSnDEtML8TQ2d6KaeliE2tbKAi8TDTJtABzRwj2Rkz842b2fe0H9Pvf4GHUzzXn2gX9y2ZjJoQEVUbHGyhR9wwqTNKBSGmU2xAP/zjnK7+C+wKqdO5tFAys20SJVLD+gSLXjtzilhLj+Lr9Fa9bTQS--ssLj7cwH8Q5v7heT--OUXCCjkqOcZg77VsuaIldQ== \ No newline at end of file diff --git a/app/config/database.yml b/app/config/database.yml new file mode 100644 index 0000000..b2e35d7 --- /dev/null +++ b/app/config/database.yml @@ -0,0 +1,84 @@ +# 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 } %> + +development: + <<: *default + database: template_application_rails_development + + # 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: template_application_rails + + # The password associated with the PostgreSQL role (username). + #password: + + # 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 + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # 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: template_application_rails_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: template_application_rails_production + username: template_application_rails + password: <%= ENV["TEMPLATE_APPLICATION_RAILS_DATABASE_PASSWORD"] %> diff --git a/app/config/environment.rb b/app/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/app/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/app/config/environments/development.rb b/app/config/environments/development.rb new file mode 100644 index 0000000..2e7fb48 --- /dev/null +++ b/app/config/environments/development.rb @@ -0,0 +1,76 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/app/config/environments/production.rb b/app/config/environments/production.rb new file mode 100644 index 0000000..bc10944 --- /dev/null +++ b/app/config/environments/production.rb @@ -0,0 +1,97 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fall back to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new(STDOUT) + .tap { |logger| logger.formatter = ::Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "template_application_rails_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/app/config/environments/test.rb b/app/config/environments/test.rb new file mode 100644 index 0000000..adbb4a6 --- /dev/null +++ b/app/config/environments/test.rb @@ -0,0 +1,64 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/app/config/importmap.rb b/app/config/importmap.rb new file mode 100644 index 0000000..909dfc5 --- /dev/null +++ b/app/config/importmap.rb @@ -0,0 +1,7 @@ +# Pin npm packages by running ./bin/importmap + +pin "application" +pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin_all_from "app/javascript/controllers", under: "controllers" diff --git a/app/config/initializers/assets.rb b/app/config/initializers/assets.rb new file mode 100644 index 0000000..2eeef96 --- /dev/null +++ b/app/config/initializers/assets.rb @@ -0,0 +1,12 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/app/config/initializers/content_security_policy.rb b/app/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..b3076b3 --- /dev/null +++ b/app/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/app/config/initializers/filter_parameter_logging.rb b/app/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..c2d89e2 --- /dev/null +++ b/app/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/app/config/initializers/inflections.rb b/app/config/initializers/inflections.rb new file mode 100644 index 0000000..3860f65 --- /dev/null +++ b/app/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/app/config/initializers/permissions_policy.rb b/app/config/initializers/permissions_policy.rb new file mode 100644 index 0000000..7db3b95 --- /dev/null +++ b/app/config/initializers/permissions_policy.rb @@ -0,0 +1,13 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide HTTP permissions policy. For further +# information see: https://developers.google.com/web/updates/2018/06/feature-policy + +# Rails.application.config.permissions_policy do |policy| +# policy.camera :none +# policy.gyroscope :none +# policy.microphone :none +# policy.usb :none +# policy.fullscreen :self +# policy.payment :self, "https://secure.example.com" +# end diff --git a/app/config/locales/en.yml b/app/config/locales/en.yml new file mode 100644 index 0000000..6c349ae --- /dev/null +++ b/app/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/app/config/puma.rb b/app/config/puma.rb new file mode 100644 index 0000000..afa809b --- /dev/null +++ b/app/config/puma.rb @@ -0,0 +1,35 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. + +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies that the worker count should equal the number of processors in production. +if ENV["RAILS_ENV"] == "production" + require "concurrent-ruby" + worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) + workers worker_count if worker_count > 1 +end + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart diff --git a/app/config/routes.rb b/app/config/routes.rb new file mode 100644 index 0000000..a125ef0 --- /dev/null +++ b/app/config/routes.rb @@ -0,0 +1,10 @@ +Rails.application.routes.draw do + # 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. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", as: :rails_health_check + + # Defines the root path route ("/") + # root "posts#index" +end diff --git a/app/config/storage.yml b/app/config/storage.yml new file mode 100644 index 0000000..4942ab6 --- /dev/null +++ b/app/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/app/db/seeds.rb b/app/db/seeds.rb new file mode 100644 index 0000000..4fbd6ed --- /dev/null +++ b/app/db/seeds.rb @@ -0,0 +1,9 @@ +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end diff --git a/app/lib/assets/.keep b/app/lib/assets/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/lib/tasks/.keep b/app/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/log/.keep b/app/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/public/404.html b/app/public/404.html new file mode 100644 index 0000000..2be3af2 --- /dev/null +++ b/app/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/app/public/422.html b/app/public/422.html new file mode 100644 index 0000000..c08eac0 --- /dev/null +++ b/app/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/app/public/500.html b/app/public/500.html new file mode 100644 index 0000000..78a030a --- /dev/null +++ b/app/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/app/public/apple-touch-icon-precomposed.png b/app/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..e69de29 diff --git a/app/public/apple-touch-icon.png b/app/public/apple-touch-icon.png new file mode 100644 index 0000000..e69de29 diff --git a/app/public/favicon.ico b/app/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/app/public/robots.txt b/app/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/app/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/app/storage/.keep b/app/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/tmp/.keep b/app/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/tmp/pids/.keep b/app/tmp/pids/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/tmp/storage/.keep b/app/tmp/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/vendor/.keep b/app/vendor/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/vendor/javascript/.keep b/app/vendor/javascript/.keep new file mode 100644 index 0000000..e69de29 From bc2feade7f9ce8a6a9594cde08f8432991856bda Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 13:21:58 -0700 Subject: [PATCH 02/76] Rename app to app-rails --- app/.dockerignore | 37 -- app/.gitattributes | 9 - app/.gitignore | 38 --- app/.ruby-version | 1 - app/Dockerfile | 62 ---- app/Gemfile | 70 ---- app/Gemfile.lock | 316 ------------------ app/Procfile.dev | 2 - app/README.md | 24 -- app/Rakefile | 6 - app/app/assets/builds/.keep | 0 app/app/assets/config/manifest.js | 4 - app/app/assets/images/.keep | 0 app/app/assets/stylesheets/application.css | 15 - app/app/assets/stylesheets/application.scss | 1 - app/app/channels/application_cable/channel.rb | 4 - .../channels/application_cable/connection.rb | 4 - app/app/controllers/application_controller.rb | 2 - app/app/controllers/concerns/.keep | 0 app/app/helpers/application_helper.rb | 2 - app/app/javascript/application.js | 3 - app/app/javascript/controllers/application.js | 9 - .../controllers/hello_controller.js | 7 - app/app/javascript/controllers/index.js | 11 - app/app/jobs/application_job.rb | 7 - app/app/mailers/application_mailer.rb | 4 - app/app/models/application_record.rb | 3 - app/app/models/concerns/.keep | 0 app/app/views/layouts/application.html.erb | 16 - app/app/views/layouts/mailer.html.erb | 13 - app/app/views/layouts/mailer.text.erb | 1 - app/bin/bundle | 109 ------ app/bin/dev | 8 - app/bin/docker-entrypoint | 8 - app/bin/importmap | 4 - app/bin/rails | 4 - app/bin/rake | 4 - app/bin/setup | 33 -- app/config.ru | 6 - app/config/application.rb | 27 -- app/config/boot.rb | 4 - app/config/cable.yml | 10 - app/config/credentials.yml.enc | 1 - app/config/database.yml | 84 ----- app/config/environment.rb | 5 - app/config/environments/development.rb | 76 ----- app/config/environments/production.rb | 97 ------ app/config/environments/test.rb | 64 ---- app/config/importmap.rb | 7 - app/config/initializers/assets.rb | 12 - .../initializers/content_security_policy.rb | 25 -- .../initializers/filter_parameter_logging.rb | 8 - app/config/initializers/inflections.rb | 16 - app/config/initializers/permissions_policy.rb | 13 - app/config/locales/en.yml | 31 -- app/config/puma.rb | 35 -- app/config/routes.rb | 10 - app/config/storage.yml | 34 -- app/db/seeds.rb | 9 - app/lib/assets/.keep | 0 app/lib/tasks/.keep | 0 app/log/.keep | 0 app/public/404.html | 67 ---- app/public/422.html | 67 ---- app/public/500.html | 66 ---- app/public/apple-touch-icon-precomposed.png | 0 app/public/apple-touch-icon.png | 0 app/public/favicon.ico | 0 app/public/robots.txt | 1 - app/storage/.keep | 0 app/tmp/.keep | 0 app/tmp/pids/.keep | 0 app/tmp/storage/.keep | 0 app/vendor/.keep | 0 app/vendor/javascript/.keep | 0 75 files changed, 1616 deletions(-) delete mode 100644 app/.dockerignore delete mode 100644 app/.gitattributes delete mode 100644 app/.gitignore delete mode 100644 app/.ruby-version delete mode 100644 app/Dockerfile delete mode 100644 app/Gemfile delete mode 100644 app/Gemfile.lock delete mode 100644 app/Procfile.dev delete mode 100644 app/README.md delete mode 100644 app/Rakefile delete mode 100644 app/app/assets/builds/.keep delete mode 100644 app/app/assets/config/manifest.js delete mode 100644 app/app/assets/images/.keep delete mode 100644 app/app/assets/stylesheets/application.css delete mode 100644 app/app/assets/stylesheets/application.scss delete mode 100644 app/app/channels/application_cable/channel.rb delete mode 100644 app/app/channels/application_cable/connection.rb delete mode 100644 app/app/controllers/application_controller.rb delete mode 100644 app/app/controllers/concerns/.keep delete mode 100644 app/app/helpers/application_helper.rb delete mode 100644 app/app/javascript/application.js delete mode 100644 app/app/javascript/controllers/application.js delete mode 100644 app/app/javascript/controllers/hello_controller.js delete mode 100644 app/app/javascript/controllers/index.js delete mode 100644 app/app/jobs/application_job.rb delete mode 100644 app/app/mailers/application_mailer.rb delete mode 100644 app/app/models/application_record.rb delete mode 100644 app/app/models/concerns/.keep delete mode 100644 app/app/views/layouts/application.html.erb delete mode 100644 app/app/views/layouts/mailer.html.erb delete mode 100644 app/app/views/layouts/mailer.text.erb delete mode 100755 app/bin/bundle delete mode 100755 app/bin/dev delete mode 100755 app/bin/docker-entrypoint delete mode 100755 app/bin/importmap delete mode 100755 app/bin/rails delete mode 100755 app/bin/rake delete mode 100755 app/bin/setup delete mode 100644 app/config.ru delete mode 100644 app/config/application.rb delete mode 100644 app/config/boot.rb delete mode 100644 app/config/cable.yml delete mode 100644 app/config/credentials.yml.enc delete mode 100644 app/config/database.yml delete mode 100644 app/config/environment.rb delete mode 100644 app/config/environments/development.rb delete mode 100644 app/config/environments/production.rb delete mode 100644 app/config/environments/test.rb delete mode 100644 app/config/importmap.rb delete mode 100644 app/config/initializers/assets.rb delete mode 100644 app/config/initializers/content_security_policy.rb delete mode 100644 app/config/initializers/filter_parameter_logging.rb delete mode 100644 app/config/initializers/inflections.rb delete mode 100644 app/config/initializers/permissions_policy.rb delete mode 100644 app/config/locales/en.yml delete mode 100644 app/config/puma.rb delete mode 100644 app/config/routes.rb delete mode 100644 app/config/storage.yml delete mode 100644 app/db/seeds.rb delete mode 100644 app/lib/assets/.keep delete mode 100644 app/lib/tasks/.keep delete mode 100644 app/log/.keep delete mode 100644 app/public/404.html delete mode 100644 app/public/422.html delete mode 100644 app/public/500.html delete mode 100644 app/public/apple-touch-icon-precomposed.png delete mode 100644 app/public/apple-touch-icon.png delete mode 100644 app/public/favicon.ico delete mode 100644 app/public/robots.txt delete mode 100644 app/storage/.keep delete mode 100644 app/tmp/.keep delete mode 100644 app/tmp/pids/.keep delete mode 100644 app/tmp/storage/.keep delete mode 100644 app/vendor/.keep delete mode 100644 app/vendor/javascript/.keep diff --git a/app/.dockerignore b/app/.dockerignore deleted file mode 100644 index 9612375..0000000 --- a/app/.dockerignore +++ /dev/null @@ -1,37 +0,0 @@ -# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. - -# Ignore git directory. -/.git/ - -# Ignore bundler config. -/.bundle - -# Ignore all environment files (except templates). -/.env* -!/.env*.erb - -# Ignore all default key files. -/config/master.key -/config/credentials/*.key - -# Ignore all logfiles and tempfiles. -/log/* -/tmp/* -!/log/.keep -!/tmp/.keep - -# Ignore pidfiles, but keep the directory. -/tmp/pids/* -!/tmp/pids/.keep - -# Ignore storage (uploaded files in development and any SQLite databases). -/storage/* -!/storage/.keep -/tmp/storage/* -!/tmp/storage/.keep - -# Ignore assets. -/node_modules/ -/app/assets/builds/* -!/app/assets/builds/.keep -/public/assets diff --git a/app/.gitattributes b/app/.gitattributes deleted file mode 100644 index 8dc4323..0000000 --- a/app/.gitattributes +++ /dev/null @@ -1,9 +0,0 @@ -# See https://git-scm.com/docs/gitattributes for more about git attribute files. - -# Mark the database schema as having been generated. -db/schema.rb linguist-generated - -# Mark any vendored files as having been vendored. -vendor/* linguist-vendored -config/credentials/*.yml.enc diff=rails_credentials -config/credentials.yml.enc diff=rails_credentials diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 9b66b15..0000000 --- a/app/.gitignore +++ /dev/null @@ -1,38 +0,0 @@ -# See https://help.github.com/articles/ignoring-files for more about ignoring files. -# -# If you find yourself ignoring temporary files generated by your text editor -# or operating system, you probably want to add a global ignore instead: -# git config --global core.excludesfile '~/.gitignore_global' - -# Ignore bundler config. -/.bundle - -# Ignore all environment files (except templates). -/.env* -!/.env*.erb - -# Ignore all logfiles and tempfiles. -/log/* -/tmp/* -!/log/.keep -!/tmp/.keep - -# Ignore pidfiles, but keep the directory. -/tmp/pids/* -!/tmp/pids/ -!/tmp/pids/.keep - -# Ignore storage (uploaded files in development and any SQLite databases). -/storage/* -!/storage/.keep -/tmp/storage/* -!/tmp/storage/ -!/tmp/storage/.keep - -/public/assets - -# Ignore master key for decrypting credentials and more. -/config/master.key - -/app/assets/builds/* -!/app/assets/builds/.keep diff --git a/app/.ruby-version b/app/.ruby-version deleted file mode 100644 index 15a2799..0000000 --- a/app/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.3.0 diff --git a/app/Dockerfile b/app/Dockerfile deleted file mode 100644 index 2c1e8b4..0000000 --- a/app/Dockerfile +++ /dev/null @@ -1,62 +0,0 @@ -# syntax = docker/dockerfile:1 - -# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile -ARG RUBY_VERSION=3.3.0 -FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base - -# Rails app lives here -WORKDIR /rails - -# Set production environment -ENV RAILS_ENV="production" \ - BUNDLE_DEPLOYMENT="1" \ - BUNDLE_PATH="/usr/local/bundle" \ - BUNDLE_WITHOUT="development" - - -# Throw-away build stage to reduce size of final image -FROM base as build - -# Install packages needed to build gems -RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config - -# Install application gems -COPY Gemfile Gemfile.lock ./ -RUN bundle install && \ - rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ - bundle exec bootsnap precompile --gemfile - -# Copy application code -COPY . . - -# Precompile bootsnap code for faster boot times -RUN bundle exec bootsnap precompile app/ lib/ - -# Precompiling assets for production without requiring secret RAILS_MASTER_KEY -RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile - - -# Final stage for app image -FROM base - -# Install packages needed for deployment -RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libvips postgresql-client && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives - -# Copy built artifacts: gems, application -COPY --from=build /usr/local/bundle /usr/local/bundle -COPY --from=build /rails /rails - -# Run and own only the runtime files as a non-root user for security -RUN useradd rails --create-home --shell /bin/bash && \ - chown -R rails:rails db log storage tmp -USER rails:rails - -# Entrypoint prepares the database. -ENTRYPOINT ["/rails/bin/docker-entrypoint"] - -# Start the server by default, this can be overwritten at runtime -EXPOSE 3000 -CMD ["./bin/rails", "server"] diff --git a/app/Gemfile b/app/Gemfile deleted file mode 100644 index f4015d2..0000000 --- a/app/Gemfile +++ /dev/null @@ -1,70 +0,0 @@ -source "https://rubygems.org" - -ruby "3.3.0" - -# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem "rails", "~> 7.1.3", ">= 7.1.3.2" - -# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem "sprockets-rails" - -# 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" - -# 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" - -# Use Dart SASS [https://github.com/rails/dartsass-rails] -gem "dartsass-rails" - -# Build JSON APIs with ease [https://github.com/rails/jbuilder] -gem "jbuilder" - -# Use Redis adapter to run Action Cable in production -# gem "redis", ">= 4.0.1" - -# 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[ windows 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 ] -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" - - # Speed up commands on slow machines / big apps [https://github.com/rails/spring] - # gem "spring" -end - -group :test do - # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] - gem "capybara" - gem "selenium-webdriver" -end diff --git a/app/Gemfile.lock b/app/Gemfile.lock deleted file mode 100644 index 1868a3d..0000000 --- a/app/Gemfile.lock +++ /dev/null @@ -1,316 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - actioncable (7.1.3.2) - actionpack (= 7.1.3.2) - activesupport (= 7.1.3.2) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - zeitwerk (~> 2.6) - actionmailbox (7.1.3.2) - actionpack (= 7.1.3.2) - activejob (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.3.2) - actionpack (= 7.1.3.2) - actionview (= 7.1.3.2) - activejob (= 7.1.3.2) - activesupport (= 7.1.3.2) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp - rails-dom-testing (~> 2.2) - actionpack (7.1.3.2) - actionview (= 7.1.3.2) - activesupport (= 7.1.3.2) - nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4) - rack-session (>= 1.0.1) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - actiontext (7.1.3.2) - actionpack (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) - globalid (>= 0.6.0) - nokogiri (>= 1.8.5) - actionview (7.1.3.2) - activesupport (= 7.1.3.2) - builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activejob (7.1.3.2) - activesupport (= 7.1.3.2) - globalid (>= 0.3.6) - activemodel (7.1.3.2) - activesupport (= 7.1.3.2) - activerecord (7.1.3.2) - activemodel (= 7.1.3.2) - activesupport (= 7.1.3.2) - timeout (>= 0.4.0) - activestorage (7.1.3.2) - actionpack (= 7.1.3.2) - activejob (= 7.1.3.2) - activerecord (= 7.1.3.2) - activesupport (= 7.1.3.2) - marcel (~> 1.0) - activesupport (7.1.3.2) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) - base64 (0.2.0) - bigdecimal (3.1.7) - bindex (0.8.1) - bootsnap (1.18.3) - msgpack (~> 1.2) - builder (3.2.4) - capybara (3.40.0) - addressable - matrix - mini_mime (>= 0.1.3) - nokogiri (~> 1.11) - rack (>= 1.6.0) - rack-test (>= 0.6.3) - regexp_parser (>= 1.5, < 3.0) - xpath (~> 3.2) - concurrent-ruby (1.2.3) - connection_pool (2.4.1) - crass (1.0.6) - dartsass-rails (0.5.0) - railties (>= 6.0.0) - sass-embedded (~> 1.63) - date (3.3.4) - debug (1.9.2) - irb (~> 1.10) - reline (>= 0.3.8) - drb (2.2.1) - erubi (1.12.0) - globalid (1.2.1) - activesupport (>= 6.1) - google-protobuf (4.26.1) - rake (>= 13) - google-protobuf (4.26.1-aarch64-linux) - rake (>= 13) - google-protobuf (4.26.1-arm64-darwin) - rake (>= 13) - google-protobuf (4.26.1-x86-linux) - rake (>= 13) - google-protobuf (4.26.1-x86_64-darwin) - rake (>= 13) - google-protobuf (4.26.1-x86_64-linux) - rake (>= 13) - i18n (1.14.4) - concurrent-ruby (~> 1.0) - importmap-rails (2.0.1) - actionpack (>= 6.0.0) - activesupport (>= 6.0.0) - railties (>= 6.0.0) - io-console (0.7.2) - irb (1.12.0) - rdoc - reline (>= 0.4.2) - jbuilder (2.11.5) - actionview (>= 5.0.0) - activesupport (>= 5.0.0) - loofah (2.22.0) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - mail (2.8.1) - mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp - marcel (1.0.4) - matrix (0.4.2) - mini_mime (1.1.5) - minitest (5.22.3) - msgpack (1.7.2) - mutex_m (0.2.0) - net-imap (0.4.10) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.2) - timeout - net-smtp (0.5.0) - net-protocol - nio4r (2.7.1) - nokogiri (1.16.3-aarch64-linux) - racc (~> 1.4) - nokogiri (1.16.3-arm-linux) - racc (~> 1.4) - nokogiri (1.16.3-arm64-darwin) - racc (~> 1.4) - nokogiri (1.16.3-x86-linux) - racc (~> 1.4) - nokogiri (1.16.3-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.16.3-x86_64-linux) - racc (~> 1.4) - pg (1.5.6) - psych (5.1.2) - stringio - public_suffix (5.0.4) - puma (6.4.2) - nio4r (~> 2.0) - racc (1.7.3) - rack (3.0.10) - rack-session (2.0.0) - rack (>= 3.0.0) - rack-test (2.1.0) - rack (>= 1.3) - rackup (2.1.0) - rack (>= 3) - webrick (~> 1.8) - rails (7.1.3.2) - actioncable (= 7.1.3.2) - actionmailbox (= 7.1.3.2) - actionmailer (= 7.1.3.2) - actionpack (= 7.1.3.2) - actiontext (= 7.1.3.2) - actionview (= 7.1.3.2) - activejob (= 7.1.3.2) - activemodel (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) - bundler (>= 1.15.0) - railties (= 7.1.3.2) - rails-dom-testing (2.2.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - railties (7.1.3.2) - actionpack (= 7.1.3.2) - activesupport (= 7.1.3.2) - irb - rackup (>= 1.0.0) - rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - zeitwerk (~> 2.6) - rake (13.1.0) - rdoc (6.6.3.1) - psych (>= 4.0.0) - regexp_parser (2.9.0) - reline (0.5.0) - io-console (~> 0.5) - rexml (3.2.6) - rubyzip (2.3.2) - sass-embedded (1.72.0-aarch64-linux-gnu) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-aarch64-linux-musl) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-arm-linux-gnueabihf) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-arm-linux-musleabihf) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-arm64-darwin) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-x86-linux-gnu) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-x86-linux-musl) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-x86_64-darwin) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-x86_64-linux-gnu) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-x86_64-linux-musl) - google-protobuf (>= 3.25, < 5.0) - selenium-webdriver (4.19.0) - base64 (~> 0.2) - rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) - websocket (~> 1.0) - sprockets (4.2.1) - concurrent-ruby (~> 1.0) - rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - stimulus-rails (1.3.3) - railties (>= 6.0.0) - stringio (3.1.0) - thor (1.3.1) - timeout (0.4.1) - turbo-rails (2.0.5) - actionpack (>= 6.0.0) - activejob (>= 6.0.0) - railties (>= 6.0.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - web-console (4.2.1) - actionview (>= 6.0.0) - activemodel (>= 6.0.0) - bindex (>= 0.4.0) - railties (>= 6.0.0) - webrick (1.8.1) - websocket (1.2.10) - websocket-driver (0.7.6) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - xpath (3.2.0) - nokogiri (~> 1.8) - zeitwerk (2.6.13) - -PLATFORMS - aarch64-linux - aarch64-linux-gnu - aarch64-linux-musl - arm-linux - arm-linux-gnueabihf - arm-linux-musleabihf - arm64-darwin - x86-linux - x86-linux-gnu - x86-linux-musl - x86_64-darwin - x86_64-linux - x86_64-linux-gnu - x86_64-linux-musl - -DEPENDENCIES - bootsnap - capybara - dartsass-rails - debug - importmap-rails - jbuilder - pg (~> 1.1) - puma (>= 5.0) - rails (~> 7.1.3, >= 7.1.3.2) - selenium-webdriver - sprockets-rails - stimulus-rails - turbo-rails - tzinfo-data - web-console - -RUBY VERSION - ruby 3.3.0p0 - -BUNDLED WITH - 2.5.6 diff --git a/app/Procfile.dev b/app/Procfile.dev deleted file mode 100644 index 852e6c7..0000000 --- a/app/Procfile.dev +++ /dev/null @@ -1,2 +0,0 @@ -web: bin/rails server -p 3000 -css: bin/rails dartsass:watch diff --git a/app/README.md b/app/README.md deleted file mode 100644 index 7db80e4..0000000 --- a/app/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# README - -This README would normally document whatever steps are necessary to get the -application up and running. - -Things you may want to cover: - -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... diff --git a/app/Rakefile b/app/Rakefile deleted file mode 100644 index 9a5ea73..0000000 --- a/app/Rakefile +++ /dev/null @@ -1,6 +0,0 @@ -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require_relative "config/application" - -Rails.application.load_tasks diff --git a/app/app/assets/builds/.keep b/app/app/assets/builds/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/app/assets/config/manifest.js b/app/app/assets/config/manifest.js deleted file mode 100644 index 4028c22..0000000 --- a/app/app/assets/config/manifest.js +++ /dev/null @@ -1,4 +0,0 @@ -//= link_tree ../images -//= link_tree ../../javascript .js -//= link_tree ../../../vendor/javascript .js -//= link_tree ../builds diff --git a/app/app/assets/images/.keep b/app/app/assets/images/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/app/assets/stylesheets/application.css b/app/app/assets/stylesheets/application.css deleted file mode 100644 index 288b9ab..0000000 --- a/app/app/assets/stylesheets/application.css +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's - * vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - */ diff --git a/app/app/assets/stylesheets/application.scss b/app/app/assets/stylesheets/application.scss deleted file mode 100644 index 8d7af90..0000000 --- a/app/app/assets/stylesheets/application.scss +++ /dev/null @@ -1 +0,0 @@ -// Sassy diff --git a/app/app/channels/application_cable/channel.rb b/app/app/channels/application_cable/channel.rb deleted file mode 100644 index d672697..0000000 --- a/app/app/channels/application_cable/channel.rb +++ /dev/null @@ -1,4 +0,0 @@ -module ApplicationCable - class Channel < ActionCable::Channel::Base - end -end diff --git a/app/app/channels/application_cable/connection.rb b/app/app/channels/application_cable/connection.rb deleted file mode 100644 index 0ff5442..0000000 --- a/app/app/channels/application_cable/connection.rb +++ /dev/null @@ -1,4 +0,0 @@ -module ApplicationCable - class Connection < ActionCable::Connection::Base - end -end diff --git a/app/app/controllers/application_controller.rb b/app/app/controllers/application_controller.rb deleted file mode 100644 index 09705d1..0000000 --- a/app/app/controllers/application_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ApplicationController < ActionController::Base -end diff --git a/app/app/controllers/concerns/.keep b/app/app/controllers/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/app/helpers/application_helper.rb b/app/app/helpers/application_helper.rb deleted file mode 100644 index de6be79..0000000 --- a/app/app/helpers/application_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ApplicationHelper -end diff --git a/app/app/javascript/application.js b/app/app/javascript/application.js deleted file mode 100644 index 0d7b494..0000000 --- a/app/app/javascript/application.js +++ /dev/null @@ -1,3 +0,0 @@ -// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails -import "@hotwired/turbo-rails" -import "controllers" diff --git a/app/app/javascript/controllers/application.js b/app/app/javascript/controllers/application.js deleted file mode 100644 index 1213e85..0000000 --- a/app/app/javascript/controllers/application.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Application } from "@hotwired/stimulus" - -const application = Application.start() - -// Configure Stimulus development experience -application.debug = false -window.Stimulus = application - -export { application } diff --git a/app/app/javascript/controllers/hello_controller.js b/app/app/javascript/controllers/hello_controller.js deleted file mode 100644 index 5975c07..0000000 --- a/app/app/javascript/controllers/hello_controller.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - connect() { - this.element.textContent = "Hello World!" - } -} diff --git a/app/app/javascript/controllers/index.js b/app/app/javascript/controllers/index.js deleted file mode 100644 index 54ad4ca..0000000 --- a/app/app/javascript/controllers/index.js +++ /dev/null @@ -1,11 +0,0 @@ -// Import and register all your controllers from the importmap under controllers/* - -import { application } from "controllers/application" - -// Eager load all controllers defined in the import map under controllers/**/*_controller -import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" -eagerLoadControllersFrom("controllers", application) - -// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) -// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" -// lazyLoadControllersFrom("controllers", application) diff --git a/app/app/jobs/application_job.rb b/app/app/jobs/application_job.rb deleted file mode 100644 index d394c3d..0000000 --- a/app/app/jobs/application_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class ApplicationJob < ActiveJob::Base - # Automatically retry jobs that encountered a deadlock - # retry_on ActiveRecord::Deadlocked - - # Most jobs are safe to ignore if the underlying records are no longer available - # discard_on ActiveJob::DeserializationError -end diff --git a/app/app/mailers/application_mailer.rb b/app/app/mailers/application_mailer.rb deleted file mode 100644 index 3c34c81..0000000 --- a/app/app/mailers/application_mailer.rb +++ /dev/null @@ -1,4 +0,0 @@ -class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" - layout "mailer" -end diff --git a/app/app/models/application_record.rb b/app/app/models/application_record.rb deleted file mode 100644 index b63caeb..0000000 --- a/app/app/models/application_record.rb +++ /dev/null @@ -1,3 +0,0 @@ -class ApplicationRecord < ActiveRecord::Base - primary_abstract_class -end diff --git a/app/app/models/concerns/.keep b/app/app/models/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/app/views/layouts/application.html.erb b/app/app/views/layouts/application.html.erb deleted file mode 100644 index a80e872..0000000 --- a/app/app/views/layouts/application.html.erb +++ /dev/null @@ -1,16 +0,0 @@ - - - - TemplateApplicationRails - - <%= csrf_meta_tags %> - <%= csp_meta_tag %> - - <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> - <%= javascript_importmap_tags %> - - - - <%= yield %> - - diff --git a/app/app/views/layouts/mailer.html.erb b/app/app/views/layouts/mailer.html.erb deleted file mode 100644 index 3aac900..0000000 --- a/app/app/views/layouts/mailer.html.erb +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - <%= yield %> - - diff --git a/app/app/views/layouts/mailer.text.erb b/app/app/views/layouts/mailer.text.erb deleted file mode 100644 index 37f0bdd..0000000 --- a/app/app/views/layouts/mailer.text.erb +++ /dev/null @@ -1 +0,0 @@ -<%= yield %> diff --git a/app/bin/bundle b/app/bin/bundle deleted file mode 100755 index 50da5fd..0000000 --- a/app/bin/bundle +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# -# This file was generated by Bundler. -# -# The application 'bundle' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require "rubygems" - -m = Module.new do - module_function - - def invoked_as_script? - File.expand_path($0) == File.expand_path(__FILE__) - end - - def env_var_version - ENV["BUNDLER_VERSION"] - end - - def cli_arg_version - return unless invoked_as_script? # don't want to hijack other binstubs - return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` - bundler_version = nil - update_index = nil - ARGV.each_with_index do |a, i| - if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) - bundler_version = a - end - next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ - bundler_version = $1 - update_index = i - end - bundler_version - end - - def gemfile - gemfile = ENV["BUNDLE_GEMFILE"] - return gemfile if gemfile && !gemfile.empty? - - File.expand_path("../Gemfile", __dir__) - end - - def lockfile - lockfile = - case File.basename(gemfile) - when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") - else "#{gemfile}.lock" - end - File.expand_path(lockfile) - end - - def lockfile_version - return unless File.file?(lockfile) - lockfile_contents = File.read(lockfile) - return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ - Regexp.last_match(1) - end - - def bundler_requirement - @bundler_requirement ||= - env_var_version || - cli_arg_version || - bundler_requirement_for(lockfile_version) - end - - def bundler_requirement_for(version) - return "#{Gem::Requirement.default}.a" unless version - - bundler_gem_version = Gem::Version.new(version) - - bundler_gem_version.approximate_recommendation - end - - def load_bundler! - ENV["BUNDLE_GEMFILE"] ||= gemfile - - activate_bundler - end - - def activate_bundler - gem_error = activation_error_handling do - gem "bundler", bundler_requirement - end - return if gem_error.nil? - require_error = activation_error_handling do - require "bundler/version" - end - return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) - warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" - exit 42 - end - - def activation_error_handling - yield - nil - rescue StandardError, LoadError => e - e - end -end - -m.load_bundler! - -if m.invoked_as_script? - load Gem.bin_path("bundler", "bundle") -end diff --git a/app/bin/dev b/app/bin/dev deleted file mode 100755 index 74ade16..0000000 --- a/app/bin/dev +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env sh - -if ! gem list foreman -i --silent; then - echo "Installing foreman..." - gem install foreman -fi - -exec foreman start -f Procfile.dev "$@" diff --git a/app/bin/docker-entrypoint b/app/bin/docker-entrypoint deleted file mode 100755 index 67ef493..0000000 --- a/app/bin/docker-entrypoint +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -e - -# If running the rails server then create or migrate existing database -if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then - ./bin/rails db:prepare -fi - -exec "${@}" diff --git a/app/bin/importmap b/app/bin/importmap deleted file mode 100755 index 36502ab..0000000 --- a/app/bin/importmap +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby - -require_relative "../config/application" -require "importmap/commands" diff --git a/app/bin/rails b/app/bin/rails deleted file mode 100755 index efc0377..0000000 --- a/app/bin/rails +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby -APP_PATH = File.expand_path("../config/application", __dir__) -require_relative "../config/boot" -require "rails/commands" diff --git a/app/bin/rake b/app/bin/rake deleted file mode 100755 index 4fbf10b..0000000 --- a/app/bin/rake +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby -require_relative "../config/boot" -require "rake" -Rake.application.run diff --git a/app/bin/setup b/app/bin/setup deleted file mode 100755 index 3cd5a9d..0000000 --- a/app/bin/setup +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env ruby -require "fileutils" - -# path to your application root. -APP_ROOT = File.expand_path("..", __dir__) - -def system!(*args) - system(*args, exception: true) -end - -FileUtils.chdir APP_ROOT do - # This script is a way to set up or update your development environment automatically. - # This script is idempotent, so that you can run it at any time and get an expectable outcome. - # Add necessary setup steps to this file. - - puts "== Installing dependencies ==" - system! "gem install bundler --conservative" - system("bundle check") || system!("bundle install") - - # puts "\n== Copying sample files ==" - # unless File.exist?("config/database.yml") - # FileUtils.cp "config/database.yml.sample", "config/database.yml" - # end - - puts "\n== Preparing database ==" - system! "bin/rails db:prepare" - - puts "\n== Removing old logs and tempfiles ==" - system! "bin/rails log:clear tmp:clear" - - puts "\n== Restarting application server ==" - system! "bin/rails restart" -end diff --git a/app/config.ru b/app/config.ru deleted file mode 100644 index 4a3c09a..0000000 --- a/app/config.ru +++ /dev/null @@ -1,6 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require_relative "config/environment" - -run Rails.application -Rails.application.load_server diff --git a/app/config/application.rb b/app/config/application.rb deleted file mode 100644 index 5448663..0000000 --- a/app/config/application.rb +++ /dev/null @@ -1,27 +0,0 @@ -require_relative "boot" - -require "rails/all" - -# Require the gems listed in Gemfile, including any gems -# you've limited to :test, :development, or :production. -Bundler.require(*Rails.groups) - -module TemplateApplicationRails - class Application < Rails::Application - # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.1 - - # Please, add to the `ignore` list any other `lib` subdirectories that do - # not contain `.rb` files, or that should not be reloaded or eager loaded. - # Common ones are `templates`, `generators`, or `middleware`, for example. - config.autoload_lib(ignore: %w(assets tasks)) - - # Configuration for the application, engines, and railties goes here. - # - # These settings can be overridden in specific environments using the files - # in config/environments, which are processed later. - # - # config.time_zone = "Central Time (US & Canada)" - # config.eager_load_paths << Rails.root.join("extras") - end -end diff --git a/app/config/boot.rb b/app/config/boot.rb deleted file mode 100644 index 988a5dd..0000000 --- a/app/config/boot.rb +++ /dev/null @@ -1,4 +0,0 @@ -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) - -require "bundler/setup" # Set up gems listed in the Gemfile. -require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/app/config/cable.yml b/app/config/cable.yml deleted file mode 100644 index 8979b1b..0000000 --- a/app/config/cable.yml +++ /dev/null @@ -1,10 +0,0 @@ -development: - adapter: async - -test: - adapter: test - -production: - adapter: redis - url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> - channel_prefix: template_application_rails_production diff --git a/app/config/credentials.yml.enc b/app/config/credentials.yml.enc deleted file mode 100644 index 8e564d2..0000000 --- a/app/config/credentials.yml.enc +++ /dev/null @@ -1 +0,0 @@ -r/2Qv1Rt0kJvM3tfeP83+2j3lOinPYglSkHRNWnrFo0q8uAKkZbB2Zhnmw4UWkXzyqnzH3EjB7VhLqtdA3j1qLQaaTCfGw7rKpgXTlniegrigJoztLM6GlJH+jNfPFl9TgrPgHwJ+OHsgNGhRqi8uStAMi+onIuIqihwkxP4N1oQnRV2ywM4wmS+JPDPcZWX7SObIiEGFm6nmWKUYFDUdAeOVVa2dfhKOD4qu4jMUeopfZbF2dsy4OknEo5kw6Qc479GSlUSnDEtML8TQ2d6KaeliE2tbKAi8TDTJtABzRwj2Rkz842b2fe0H9Pvf4GHUzzXn2gX9y2ZjJoQEVUbHGyhR9wwqTNKBSGmU2xAP/zjnK7+C+wKqdO5tFAys20SJVLD+gSLXjtzilhLj+Lr9Fa9bTQS--ssLj7cwH8Q5v7heT--OUXCCjkqOcZg77VsuaIldQ== \ No newline at end of file diff --git a/app/config/database.yml b/app/config/database.yml deleted file mode 100644 index b2e35d7..0000000 --- a/app/config/database.yml +++ /dev/null @@ -1,84 +0,0 @@ -# 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 } %> - -development: - <<: *default - database: template_application_rails_development - - # 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: template_application_rails - - # The password associated with the PostgreSQL role (username). - #password: - - # 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 - - # The TCP port the server listens on. Defaults to 5432. - # If your server runs on a different port number, change accordingly. - #port: 5432 - - # Schema search path. The server defaults to $user,public - #schema_search_path: myapp,sharedapp,public - - # 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: template_application_rails_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: template_application_rails_production - username: template_application_rails - password: <%= ENV["TEMPLATE_APPLICATION_RAILS_DATABASE_PASSWORD"] %> diff --git a/app/config/environment.rb b/app/config/environment.rb deleted file mode 100644 index cac5315..0000000 --- a/app/config/environment.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Load the Rails application. -require_relative "application" - -# Initialize the Rails application. -Rails.application.initialize! diff --git a/app/config/environments/development.rb b/app/config/environments/development.rb deleted file mode 100644 index 2e7fb48..0000000 --- a/app/config/environments/development.rb +++ /dev/null @@ -1,76 +0,0 @@ -require "active_support/core_ext/integer/time" - -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # In the development environment your application's code is reloaded any time - # it changes. This slows down response time but is perfect for development - # since you don't have to restart the web server when you make code changes. - config.enable_reloading = true - - # Do not eager load code on boot. - config.eager_load = false - - # Show full error reports. - config.consider_all_requests_local = true - - # Enable server timing - config.server_timing = true - - # Enable/disable caching. By default caching is disabled. - # Run rails dev:cache to toggle caching. - if Rails.root.join("tmp/caching-dev.txt").exist? - config.action_controller.perform_caching = true - config.action_controller.enable_fragment_cache_logging = true - - config.cache_store = :memory_store - config.public_file_server.headers = { - "Cache-Control" => "public, max-age=#{2.days.to_i}" - } - else - config.action_controller.perform_caching = false - - config.cache_store = :null_store - end - - # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local - - # Don't care if the mailer can't send. - config.action_mailer.raise_delivery_errors = false - - config.action_mailer.perform_caching = false - - # Print deprecation notices to the Rails logger. - config.active_support.deprecation = :log - - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - - # Raise an error on page load if there are pending migrations. - config.active_record.migration_error = :page_load - - # Highlight code that triggered database queries in logs. - config.active_record.verbose_query_logs = true - - # Highlight code that enqueued background job in logs. - config.active_job.verbose_enqueue_logs = true - - # Suppress logger output for asset requests. - config.assets.quiet = true - - # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true - - # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true - - # Uncomment if you wish to allow Action Cable access from any origin. - # config.action_cable.disable_request_forgery_protection = true - - # Raise error when a before_action's only/except options reference missing actions - config.action_controller.raise_on_missing_callback_actions = true -end diff --git a/app/config/environments/production.rb b/app/config/environments/production.rb deleted file mode 100644 index bc10944..0000000 --- a/app/config/environments/production.rb +++ /dev/null @@ -1,97 +0,0 @@ -require "active_support/core_ext/integer/time" - -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # Code is not reloaded between requests. - config.enable_reloading = false - - # Eager load code on boot. This eager loads most of Rails and - # your application in memory, allowing both threaded web servers - # and those relying on copy on write to perform better. - # Rake tasks automatically ignore this option for performance. - config.eager_load = true - - # Full error reports are disabled and caching is turned on. - config.consider_all_requests_local = false - config.action_controller.perform_caching = true - - # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment - # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). - # config.require_master_key = true - - # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. - # config.public_file_server.enabled = false - - # Compress CSS using a preprocessor. - # config.assets.css_compressor = :sass - - # Do not fall back to assets pipeline if a precompiled asset is missed. - config.assets.compile = false - - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.asset_host = "http://assets.example.com" - - # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache - # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX - - # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local - - # Mount Action Cable outside main process or domain. - # config.action_cable.mount_path = nil - # config.action_cable.url = "wss://example.com/cable" - # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] - - # Assume all access to the app is happening through a SSL-terminating reverse proxy. - # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. - # config.assume_ssl = true - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true - - # Log to STDOUT by default - config.logger = ActiveSupport::Logger.new(STDOUT) - .tap { |logger| logger.formatter = ::Logger::Formatter.new } - .then { |logger| ActiveSupport::TaggedLogging.new(logger) } - - # Prepend all log lines with the following tags. - config.log_tags = [ :request_id ] - - # "info" includes generic and useful information about system operation, but avoids logging too much - # information to avoid inadvertent exposure of personally identifiable information (PII). If you - # want to log everything, set the level to "debug". - config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") - - # Use a different cache store in production. - # config.cache_store = :mem_cache_store - - # Use a real queuing backend for Active Job (and separate queues per environment). - # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "template_application_rails_production" - - config.action_mailer.perform_caching = false - - # Ignore bad email addresses and do not raise email delivery errors. - # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = true - - # Don't log any deprecations. - config.active_support.report_deprecations = false - - # Do not dump schema after migrations. - config.active_record.dump_schema_after_migration = false - - # Enable DNS rebinding protection and other `Host` header attacks. - # config.hosts = [ - # "example.com", # Allow requests from example.com - # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` - # ] - # Skip DNS rebinding protection for the default health check endpoint. - # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } -end diff --git a/app/config/environments/test.rb b/app/config/environments/test.rb deleted file mode 100644 index adbb4a6..0000000 --- a/app/config/environments/test.rb +++ /dev/null @@ -1,64 +0,0 @@ -require "active_support/core_ext/integer/time" - -# The test environment is used exclusively to run your application's -# test suite. You never need to work with it otherwise. Remember that -# your test database is "scratch space" for the test suite and is wiped -# and recreated between test runs. Don't rely on the data there! - -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # While tests run files are not watched, reloading is not necessary. - config.enable_reloading = false - - # Eager loading loads your entire application. When running a single test locally, - # this is usually not necessary, and can slow down your test suite. However, it's - # recommended that you enable it in continuous integration systems to ensure eager - # loading is working properly before deploying your code. - config.eager_load = ENV["CI"].present? - - # Configure public file server for tests with Cache-Control for performance. - config.public_file_server.enabled = true - config.public_file_server.headers = { - "Cache-Control" => "public, max-age=#{1.hour.to_i}" - } - - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - config.cache_store = :null_store - - # Render exception templates for rescuable exceptions and raise for other exceptions. - config.action_dispatch.show_exceptions = :rescuable - - # Disable request forgery protection in test environment. - config.action_controller.allow_forgery_protection = false - - # Store uploaded files on the local file system in a temporary directory. - config.active_storage.service = :test - - config.action_mailer.perform_caching = false - - # Tell Action Mailer not to deliver emails to the real world. - # The :test delivery method accumulates sent emails in the - # ActionMailer::Base.deliveries array. - config.action_mailer.delivery_method = :test - - # Print deprecation notices to the stderr. - config.active_support.deprecation = :stderr - - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - - # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true - - # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true - - # Raise error when a before_action's only/except options reference missing actions - config.action_controller.raise_on_missing_callback_actions = true -end diff --git a/app/config/importmap.rb b/app/config/importmap.rb deleted file mode 100644 index 909dfc5..0000000 --- a/app/config/importmap.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Pin npm packages by running ./bin/importmap - -pin "application" -pin "@hotwired/turbo-rails", to: "turbo.min.js" -pin "@hotwired/stimulus", to: "stimulus.min.js" -pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" -pin_all_from "app/javascript/controllers", under: "controllers" diff --git a/app/config/initializers/assets.rb b/app/config/initializers/assets.rb deleted file mode 100644 index 2eeef96..0000000 --- a/app/config/initializers/assets.rb +++ /dev/null @@ -1,12 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = "1.0" - -# Add additional assets to the asset load path. -# Rails.application.config.assets.paths << Emoji.images_path - -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in the app/assets -# folder are already added. -# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/app/config/initializers/content_security_policy.rb b/app/config/initializers/content_security_policy.rb deleted file mode 100644 index b3076b3..0000000 --- a/app/config/initializers/content_security_policy.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Define an application-wide content security policy. -# See the Securing Rails Applications Guide for more information: -# https://guides.rubyonrails.org/security.html#content-security-policy-header - -# Rails.application.configure do -# config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" -# end -# -# # Generate session nonces for permitted importmap, inline scripts, and inline styles. -# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } -# config.content_security_policy_nonce_directives = %w(script-src style-src) -# -# # Report violations without enforcing the policy. -# # config.content_security_policy_report_only = true -# end diff --git a/app/config/initializers/filter_parameter_logging.rb b/app/config/initializers/filter_parameter_logging.rb deleted file mode 100644 index c2d89e2..0000000 --- a/app/config/initializers/filter_parameter_logging.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. -# Use this to limit dissemination of sensitive information. -# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn -] diff --git a/app/config/initializers/inflections.rb b/app/config/initializers/inflections.rb deleted file mode 100644 index 3860f65..0000000 --- a/app/config/initializers/inflections.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new inflection rules using the following format. Inflections -# are locale specific, and you may define rules for as many different -# locales as you wish. All of these examples are active by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.plural /^(ox)$/i, "\\1en" -# inflect.singular /^(ox)en/i, "\\1" -# inflect.irregular "person", "people" -# inflect.uncountable %w( fish sheep ) -# end - -# These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym "RESTful" -# end diff --git a/app/config/initializers/permissions_policy.rb b/app/config/initializers/permissions_policy.rb deleted file mode 100644 index 7db3b95..0000000 --- a/app/config/initializers/permissions_policy.rb +++ /dev/null @@ -1,13 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Define an application-wide HTTP permissions policy. For further -# information see: https://developers.google.com/web/updates/2018/06/feature-policy - -# Rails.application.config.permissions_policy do |policy| -# policy.camera :none -# policy.gyroscope :none -# policy.microphone :none -# policy.usb :none -# policy.fullscreen :self -# policy.payment :self, "https://secure.example.com" -# end diff --git a/app/config/locales/en.yml b/app/config/locales/en.yml deleted file mode 100644 index 6c349ae..0000000 --- a/app/config/locales/en.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Files in the config/locales directory are used for internationalization and -# are automatically loaded by Rails. If you want to use locales other than -# English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t "hello" -# -# In views, this is aliased to just `t`: -# -# <%= t("hello") %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# To learn more about the API, please read the Rails Internationalization guide -# at https://guides.rubyonrails.org/i18n.html. -# -# Be aware that YAML interprets the following case-insensitive strings as -# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings -# must be quoted to be interpreted as strings. For example: -# -# en: -# "yes": yup -# enabled: "ON" - -en: - hello: "Hello world" diff --git a/app/config/puma.rb b/app/config/puma.rb deleted file mode 100644 index afa809b..0000000 --- a/app/config/puma.rb +++ /dev/null @@ -1,35 +0,0 @@ -# This configuration file will be evaluated by Puma. The top-level methods that -# are invoked here are part of Puma's configuration DSL. For more information -# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. - -# Puma can serve each request in a thread from an internal thread pool. -# The `threads` method setting takes two numbers: a minimum and maximum. -# Any libraries that use thread pools should be configured to match -# the maximum value specified for Puma. Default is set to 5 threads for minimum -# and maximum; this matches the default thread size of Active Record. -max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } -min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } -threads min_threads_count, max_threads_count - -# Specifies that the worker count should equal the number of processors in production. -if ENV["RAILS_ENV"] == "production" - require "concurrent-ruby" - worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) - workers worker_count if worker_count > 1 -end - -# Specifies the `worker_timeout` threshold that Puma will use to wait before -# terminating a worker in development environments. -worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" - -# Specifies the `port` that Puma will listen on to receive requests; default is 3000. -port ENV.fetch("PORT") { 3000 } - -# Specifies the `environment` that Puma will run in. -environment ENV.fetch("RAILS_ENV") { "development" } - -# Specifies the `pidfile` that Puma will use. -pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } - -# Allow puma to be restarted by `bin/rails restart` command. -plugin :tmp_restart diff --git a/app/config/routes.rb b/app/config/routes.rb deleted file mode 100644 index a125ef0..0000000 --- a/app/config/routes.rb +++ /dev/null @@ -1,10 +0,0 @@ -Rails.application.routes.draw do - # 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. - # Can be used by load balancers and uptime monitors to verify that the app is live. - get "up" => "rails/health#show", as: :rails_health_check - - # Defines the root path route ("/") - # root "posts#index" -end diff --git a/app/config/storage.yml b/app/config/storage.yml deleted file mode 100644 index 4942ab6..0000000 --- a/app/config/storage.yml +++ /dev/null @@ -1,34 +0,0 @@ -test: - service: Disk - root: <%= Rails.root.join("tmp/storage") %> - -local: - service: Disk - root: <%= Rails.root.join("storage") %> - -# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) -# amazon: -# service: S3 -# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> -# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> -# region: us-east-1 -# bucket: your_own_bucket-<%= Rails.env %> - -# Remember not to checkin your GCS keyfile to a repository -# google: -# service: GCS -# project: your_project -# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> -# bucket: your_own_bucket-<%= Rails.env %> - -# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) -# microsoft: -# service: AzureStorage -# storage_account_name: your_account_name -# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> -# container: your_container_name-<%= Rails.env %> - -# mirror: -# service: Mirror -# primary: local -# mirrors: [ amazon, google, microsoft ] diff --git a/app/db/seeds.rb b/app/db/seeds.rb deleted file mode 100644 index 4fbd6ed..0000000 --- a/app/db/seeds.rb +++ /dev/null @@ -1,9 +0,0 @@ -# This file should ensure the existence of records required to run the application in every environment (production, -# development, test). The code here should be idempotent so that it can be executed at any point in every environment. -# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). -# -# Example: -# -# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| -# MovieGenre.find_or_create_by!(name: genre_name) -# end diff --git a/app/lib/assets/.keep b/app/lib/assets/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/lib/tasks/.keep b/app/lib/tasks/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/log/.keep b/app/log/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/public/404.html b/app/public/404.html deleted file mode 100644 index 2be3af2..0000000 --- a/app/public/404.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - The page you were looking for doesn't exist (404) - - - - - - -
-
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/app/public/422.html b/app/public/422.html deleted file mode 100644 index c08eac0..0000000 --- a/app/public/422.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - The change you wanted was rejected (422) - - - - - - -
-
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/app/public/500.html b/app/public/500.html deleted file mode 100644 index 78a030a..0000000 --- a/app/public/500.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - We're sorry, but something went wrong (500) - - - - - - -
-
-

We're sorry, but something went wrong.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/app/public/apple-touch-icon-precomposed.png b/app/public/apple-touch-icon-precomposed.png deleted file mode 100644 index e69de29..0000000 diff --git a/app/public/apple-touch-icon.png b/app/public/apple-touch-icon.png deleted file mode 100644 index e69de29..0000000 diff --git a/app/public/favicon.ico b/app/public/favicon.ico deleted file mode 100644 index e69de29..0000000 diff --git a/app/public/robots.txt b/app/public/robots.txt deleted file mode 100644 index c19f78a..0000000 --- a/app/public/robots.txt +++ /dev/null @@ -1 +0,0 @@ -# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/app/storage/.keep b/app/storage/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/tmp/.keep b/app/tmp/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/tmp/pids/.keep b/app/tmp/pids/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/tmp/storage/.keep b/app/tmp/storage/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/vendor/.keep b/app/vendor/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/vendor/javascript/.keep b/app/vendor/javascript/.keep deleted file mode 100644 index e69de29..0000000 From b93085d167bd436ca25690e174c6160773f6eb8f Mon Sep 17 00:00:00 2001 From: Rocket Date: Mon, 1 Apr 2024 16:00:34 -0700 Subject: [PATCH 03/76] Setup default application --- app-rails/.gitignore | 11 + app-rails/.rspec | 1 + app-rails/.rubocop.yml | 5 + app-rails/Dockerfile | 87 ++- app-rails/Gemfile | 39 +- app-rails/Gemfile.lock | 170 ++++-- app-rails/Makefile | 202 +++++++ app-rails/Procfile.dev | 4 +- app-rails/app/assets/config/manifest.js | 13 + .../app/assets/stylesheets/_uswds-theme.scss | 33 ++ .../app/assets/stylesheets/application.scss | 47 +- .../app/controllers/application_controller.rb | 10 + app-rails/app/helpers/application_helper.rb | 8 + app-rails/app/helpers/uswds_form_builder.rb | 192 +++++++ app-rails/app/javascript/application.js | 1 + .../app/views/layouts/application.html.erb | 37 +- app-rails/bin/db-migrate | 9 + app-rails/bin/dev | 16 +- app-rails/bin/rubocop | 27 + app-rails/bin/wait-for-local-postgres.sh | 51 ++ app-rails/db/seeds.rb | 12 + app-rails/db/seeds/.keep | 0 .../lib/generators/locale/locale_generator.rb | 21 + .../locale/templates/model_locale.yml | 4 + .../locale/templates/view_locale.yml | 6 + app-rails/local.env.example | 21 + app-rails/package-lock.json | 500 ++++++++++++++++++ app-rails/package.json | 14 + 28 files changed, 1471 insertions(+), 70 deletions(-) create mode 100644 app-rails/.rspec create mode 100644 app-rails/.rubocop.yml create mode 100644 app-rails/Makefile create mode 100644 app-rails/app/assets/stylesheets/_uswds-theme.scss create mode 100644 app-rails/app/helpers/uswds_form_builder.rb create mode 100755 app-rails/bin/db-migrate create mode 100755 app-rails/bin/rubocop create mode 100755 app-rails/bin/wait-for-local-postgres.sh create mode 100644 app-rails/db/seeds/.keep create mode 100644 app-rails/lib/generators/locale/locale_generator.rb create mode 100644 app-rails/lib/generators/locale/templates/model_locale.yml create mode 100644 app-rails/lib/generators/locale/templates/view_locale.yml create mode 100644 app-rails/local.env.example create mode 100644 app-rails/package-lock.json create mode 100644 app-rails/package.json diff --git a/app-rails/.gitignore b/app-rails/.gitignore index 9b66b15..9f92f00 100644 --- a/app-rails/.gitignore +++ b/app-rails/.gitignore @@ -7,8 +7,13 @@ # Ignore bundler config. /.bundle +# Ignore installed gems. +/vendor/bundle/* +!/vendor/bundle/.keep + # Ignore all environment files (except templates). /.env* +/*.env* !/.env*.erb # Ignore all logfiles and tempfiles. @@ -34,5 +39,11 @@ # Ignore master key for decrypting credentials and more. /config/master.key +# Ignore development log. +/log/development.log + /app/assets/builds/* !/app/assets/builds/.keep + +/node_modules/* +!/node_modules/.keep diff --git a/app-rails/.rspec b/app-rails/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/app-rails/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/app-rails/.rubocop.yml b/app-rails/.rubocop.yml new file mode 100644 index 0000000..64099d0 --- /dev/null +++ b/app-rails/.rubocop.yml @@ -0,0 +1,5 @@ +require: + - rubocop-rspec +inherit_gem: + pundit: config/rubocop-rspec.yml + rubocop-rails-omakase: rubocop.yml diff --git a/app-rails/Dockerfile b/app-rails/Dockerfile index 2c1e8b4..dccbebd 100644 --- a/app-rails/Dockerfile +++ b/app-rails/Dockerfile @@ -2,6 +2,11 @@ # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile ARG RUBY_VERSION=3.3.0 + + +########################################################################################## +# BASE: Shared base docker image +########################################################################################## FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base # Rails app lives here @@ -10,53 +15,91 @@ WORKDIR /rails # Set production environment ENV RAILS_ENV="production" \ BUNDLE_DEPLOYMENT="1" \ - BUNDLE_PATH="/usr/local/bundle" \ - BUNDLE_WITHOUT="development" + BUNDLE_PATH="/usr/local/bundle" +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 -# Throw-away build stage to reduce size of final image + +########################################################################################## +# BUILD: Throw-away build stage +########################################################################################## FROM base as build # Install packages needed to build gems RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config - -# Install application gems -COPY Gemfile Gemfile.lock ./ -RUN bundle install && \ - rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ - bundle exec bootsnap precompile --gemfile + apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config npm # Copy application code COPY . . +# Install npm packages +RUN npm install + + +########################################################################################## +# DEV: Used for development and test +########################################################################################## +FROM build as dev + +ENV RAILS_ENV="development" + +# Install packages needed for development +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y postgresql-client graphviz && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems for development +COPY Gemfile Gemfile.lock ./ +RUN bundle config set --local without production && \ + bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git + +CMD ["./bin/dev"] + + +########################################################################################## +# RELEASE-BUILD: Throw-away build stage for RELEASE +########################################################################################## +FROM build as release-build + +# Install application gems for production +COPY Gemfile Gemfile.lock ./ +RUN bundle config set --local without development test && \ + bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git + # Precompile bootsnap code for faster boot times -RUN bundle exec bootsnap precompile app/ lib/ +RUN bundle exec bootsnap precompile --gemfile app/ lib/ # Precompiling assets for production without requiring secret RAILS_MASTER_KEY RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile -# Final stage for app image -FROM base +########################################################################################## +# RELEASE: Used for production +########################################################################################## +FROM base as release # Install packages needed for deployment RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libvips postgresql-client && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives + apt-get install -y --no-install-recommends unzip python3-venv python-is-python3 curl libvips postgresql-client && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives && \ + curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip" && \ + unzip awscli-bundle.zip && \ + ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws && \ + rm -rf ./awscli-bundle awscli-bundle.zip + +# Install custom db migrate script +COPY bin/db-migrate /usr/bin/ # Copy built artifacts: gems, application -COPY --from=build /usr/local/bundle /usr/local/bundle -COPY --from=build /rails /rails +COPY --from=release-build /usr/local/bundle /usr/local/bundle +COPY --from=release-build /rails /rails # Run and own only the runtime files as a non-root user for security RUN useradd rails --create-home --shell /bin/bash && \ chown -R rails:rails db log storage tmp USER rails:rails -# Entrypoint prepares the database. -ENTRYPOINT ["/rails/bin/docker-entrypoint"] - -# Start the server by default, this can be overwritten at runtime -EXPOSE 3000 CMD ["./bin/rails", "server"] diff --git a/app-rails/Gemfile b/app-rails/Gemfile index f4015d2..6b28c31 100644 --- a/app-rails/Gemfile +++ b/app-rails/Gemfile @@ -14,6 +14,9 @@ gem "pg", "~> 1.1" # Use the Puma web server [https://github.com/puma/puma] gem "puma", ">= 5.0" +# Support Sass stylesheets +gem "cssbundling-rails", "~> 1.4" + # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] gem "importmap-rails" @@ -23,9 +26,6 @@ gem "turbo-rails" # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] gem "stimulus-rails" -# Use Dart SASS [https://github.com/rails/dartsass-rails] -gem "dartsass-rails" - # Build JSON APIs with ease [https://github.com/rails/jbuilder] gem "jbuilder" @@ -41,18 +41,46 @@ gem "jbuilder" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] +# Support localized routes (e.g. "/es-us/sobre-nosotros" instead of "/about-us") +gem "route_translator" + # 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" +# File uploads: AWS S3 +gem "active_storage_validations" + +# HTTP client for external API calls +gem "faraday" + +# Create fake data (outside of dev/test block to support seeding lower environments) +gem "faker", "~> 3.2" + group :development, :test do + # Support .env files + gem "dotenv" + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri windows ] + + # Testing framework + gem "factory_bot_rails" + gem "rspec-rails", "~> 6.1.0" + + # Generate ERDs + gem "rails-erd" end group :development do + gem "rubocop-rails-omakase", require: false + gem "rubocop-rspec", require: false + + # Test notifications locally without sending the emails + gem "letter_opener" + # Use console on exceptions pages [https://github.com/rails/web-console] gem "web-console" @@ -68,3 +96,8 @@ group :test do gem "capybara" gem "selenium-webdriver" end + +group :production do + # Add plugin for pg gem to support AWS RDS IAM + gem "pg-aws_rds_iam", "~> 0.5.0" +end diff --git a/app-rails/Gemfile.lock b/app-rails/Gemfile.lock index 1868a3d..65b83f4 100644 --- a/app-rails/Gemfile.lock +++ b/app-rails/Gemfile.lock @@ -50,6 +50,11 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) + active_storage_validations (1.1.4) + activejob (>= 5.2.0) + activemodel (>= 5.2.0) + activestorage (>= 5.2.0) + activesupport (>= 5.2.0) activejob (7.1.3.2) activesupport (= 7.1.3.2) globalid (>= 0.3.6) @@ -77,6 +82,19 @@ GEM tzinfo (~> 2.0) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) + aws-eventstream (1.3.0) + aws-partitions (1.905.0) + aws-sdk-core (3.191.5) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-rds (1.223.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.8.0) + aws-eventstream (~> 1, >= 1.0.2) base64 (0.2.0) bigdecimal (3.1.7) bindex (0.8.1) @@ -92,32 +110,34 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + childprocess (5.0.0) + choice (0.2.0) concurrent-ruby (1.2.3) connection_pool (2.4.1) crass (1.0.6) - dartsass-rails (0.5.0) + cssbundling-rails (1.4.0) railties (>= 6.0.0) - sass-embedded (~> 1.63) date (3.3.4) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) + diff-lcs (1.5.1) + dotenv (3.1.0) drb (2.2.1) erubi (1.12.0) + factory_bot (6.4.6) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.3) + factory_bot (~> 6.4) + railties (>= 5.0.0) + faker (3.3.0) + i18n (>= 1.8.11, < 2) + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-net_http (3.1.0) + net-http globalid (1.2.1) activesupport (>= 6.1) - google-protobuf (4.26.1) - rake (>= 13) - google-protobuf (4.26.1-aarch64-linux) - rake (>= 13) - google-protobuf (4.26.1-arm64-darwin) - rake (>= 13) - google-protobuf (4.26.1-x86-linux) - rake (>= 13) - google-protobuf (4.26.1-x86_64-darwin) - rake (>= 13) - google-protobuf (4.26.1-x86_64-linux) - rake (>= 13) i18n (1.14.4) concurrent-ruby (~> 1.0) importmap-rails (2.0.1) @@ -131,6 +151,14 @@ GEM jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) + jmespath (1.6.2) + json (2.7.1) + language_server-protocol (3.17.0.3) + launchy (3.0.0) + addressable (~> 2.8) + childprocess (~> 5.0) + letter_opener (1.10.0) + launchy (>= 2.2, < 4) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -145,6 +173,8 @@ GEM minitest (5.22.3) msgpack (1.7.2) mutex_m (0.2.0) + net-http (0.4.1) + uri net-imap (0.4.10) date net-protocol @@ -167,7 +197,14 @@ GEM racc (~> 1.4) nokogiri (1.16.3-x86_64-linux) racc (~> 1.4) + parallel (1.24.0) + parser (3.3.0.5) + ast (~> 2.4.1) + racc pg (1.5.6) + pg-aws_rds_iam (0.5.0) + aws-sdk-rds (~> 1.0) + pg (~> 1.1) psych (5.1.2) stringio public_suffix (5.0.4) @@ -200,6 +237,11 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) + rails-erd (1.7.2) + activerecord (>= 4.2) + activesupport (>= 4.2) + choice (~> 0.2.0) + ruby-graphviz (~> 1.2) rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) @@ -211,6 +253,7 @@ GEM rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) + rainbow (3.1.1) rake (13.1.0) rdoc (6.6.3.1) psych (>= 4.0.0) @@ -218,27 +261,70 @@ GEM reline (0.5.0) io-console (~> 0.5) rexml (3.2.6) + route_translator (14.1.1) + actionpack (>= 6.1, < 7.2) + activesupport (>= 6.1, < 7.2) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (6.1.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.1) + rubocop (1.62.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.2) + parser (>= 3.3.0.4) + rubocop-capybara (2.20.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.25.1) + rubocop (~> 1.41) + rubocop-minitest (0.35.0) + rubocop (>= 1.61, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-performance (1.21.0) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.24.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails-omakase (1.0.0) + rubocop + rubocop-minitest + rubocop-performance + rubocop-rails + rubocop-rspec (2.28.0) + rubocop (~> 1.40) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + rubocop-rspec_rails (~> 2.28) + rubocop-rspec_rails (2.28.2) + rubocop (~> 1.40) + ruby-graphviz (1.2.5) + rexml + ruby-progressbar (1.13.0) rubyzip (2.3.2) - sass-embedded (1.72.0-aarch64-linux-gnu) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-aarch64-linux-musl) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-arm-linux-gnueabihf) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-arm-linux-musleabihf) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-arm64-darwin) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-x86-linux-gnu) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-x86-linux-musl) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-x86_64-darwin) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-x86_64-linux-gnu) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.72.0-x86_64-linux-musl) - google-protobuf (>= 3.25, < 5.0) selenium-webdriver (4.19.0) base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) @@ -262,6 +348,8 @@ GEM railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unicode-display_width (2.5.0) + uri (0.13.0) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -293,15 +381,27 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + active_storage_validations bootsnap capybara - dartsass-rails + cssbundling-rails (~> 1.4) debug + dotenv + factory_bot_rails + faker (~> 3.2) + faraday importmap-rails jbuilder + letter_opener pg (~> 1.1) + pg-aws_rds_iam (~> 0.5.0) puma (>= 5.0) rails (~> 7.1.3, >= 7.1.3.2) + rails-erd + route_translator + rspec-rails (~> 6.1.0) + rubocop-rails-omakase + rubocop-rspec selenium-webdriver sprockets-rails stimulus-rails diff --git a/app-rails/Makefile b/app-rails/Makefile new file mode 100644 index 0000000..e454685 --- /dev/null +++ b/app-rails/Makefile @@ -0,0 +1,202 @@ +.PHONY : \ + release-build \ + test + +.DEFAULT_GOAL := help + +# Check that given variables are set and all have non-empty values, +# die with an error otherwise. +# +# Params: +# 1. Variable name(s) to test. +# 2. (optional) Error message to print. +# Based off of https://stackoverflow.com/questions/10858261/how-to-abort-makefile-if-variable-not-set +check_defined = \ + $(strip $(foreach 1,$1, \ + $(call __check_defined,$1,$(strip $(value 2))))) +__check_defined = \ + $(if $(value $1),, \ + $(error Undefined $1$(if $2, ($2))$(if $(value @), \ + required by target `$@'))) + +################################################## +# Constants +################################################## + +APP_NAME := app + +# Support other container tools like `finch` +ifdef CONTAINER_CMD + DOCKER_CMD := $(CONTAINER_CMD) +else + DOCKER_CMD := docker +endif + +# By default, all rails commands will run inside of the docker container +# if you wish to run this natively, add RAILS_RUN_APPROACH=local to your environment vars +# You can set this by either running `export RAILS_RUN_APPROACH=local` in your shell or add +# it to your ~/.zshrc file (and run `source ~/.zshrc`) +ifeq "$(RAILS_RUN_APPROACH)" "local" +BUNDLE_EXEC_CMD := bundle exec +else +BUNDLE_EXEC_CMD := $(DOCKER_CMD) compose run $(DOCKER_EXEC_ARGS) --rm $(APP_NAME) bundle exec +endif + +ifeq "$(RAILS_RUN_APPROACH)" "local" +RAILS_RUN_CMD := bundle exec rails +else +RAILS_RUN_CMD := $(DOCKER_CMD) compose run $(DOCKER_EXEC_ARGS) --rm $(APP_NAME) bundle exec rails +endif + +ifeq "$(RAILS_RUN_APPROACH)" "local" +RUBY_RUN_CMD := +else +RUBY_RUN_CMD := $(DOCKER_CMD) compose run $(DOCKER_EXEC_ARGS) --rm $(APP_NAME) +endif + +# Docker user configuration +# This logic is to avoid issues with permissions and mounting local volumes, +# which should be owned by the same UID for Linux distros. Mac OS can use root, +# but it is best practice to run things as with least permission where possible + +# Can be set by adding user= and/ or uid= after the make command +# If variables are not set explicitly: try looking up values from current +# environment, otherwise fixed defaults. +# uid= defaults to 0 if user= set (which makes sense if user=root, otherwise you +# probably want to set uid as well). +ifeq ($(user),) +RUN_USER ?= $(or $(strip $(USER)),nodummy) +RUN_UID ?= $(or $(strip $(shell id -u)),4000) +else +RUN_USER = $(user) +RUN_UID = $(or $(strip $(uid)),0) +endif + +export RUN_USER +export RUN_UID + +################################################## +# Setup +################################################## + +.env: local.env.example + @([ -f .env ] && echo ".env file already exists, but local.env.example is newer (or you just switched branches), check for any updates" && touch .env) || cp local.env.example .env + +init-container: ## Initialize the project for running in a container +init-container: .env build init-db + +init-native: ## Initialize the project for running natively +init-native: .env deps-native init-db + +init-db: ## Initialize the project database +init-db: .env db-up wait-on-db db-migrate db-test-prepare db-seed + +deps-native: ## Install dependencies + bundle install + npm install + +clean-native: ## Remove native installs + rm -rf node_modules + git checkout node_modules/.keep + rm -rf app/builds + git checkout app/assets/builds/.keep + rm -rf tmp/* + rm -rf vendor/bundle + git checkout vendor/bundle/.keep + +clean-container: ## Remove just the container related volumes + $(DOCKER_CMD) compose down --volumes $(APP_NAME) + +################################################## +# Build & Run +################################################## + +release-build: + docker buildx build \ + --platform=linux/amd64 \ + --build-arg RUN_USER=$(RUN_USER) \ + --build-arg RUN_UID=$(RUN_UID) \ + $(OPTS) \ + . + +build: ## Build the Docker container + $(DOCKER_CMD) compose build $(APP_NAME) + +clean-volumes: ## Remove container volumes (which includes the DB state) + $(DOCKER_CMD) compose down --volumes + +start-native: ## Run Rails natively, outside Docker +start-native: db-up + ./bin/dev + +start-container: ## Run within Docker + $(DOCKER_CMD) compose up $(APP_NAME) + +stop-containers: ## Stop Docker + $(DOCKER_CMD) compose down + +################################################## +# Database +################################################## + +db-up: ## Run just the database container + $(DOCKER_CMD) compose up --remove-orphans --detach database + +db-migrate: ## Run database migrations + $(RAILS_RUN_CMD) db:migrate + +db-test-prepare: ## Prepare the test database + $(RAILS_RUN_CMD) db:test:prepare + +db-seed: ## Seed the database + $(RAILS_RUN_CMD) db:seed + +db-reset: ## Reset the database + $(RAILS_RUN_CMD) db:reset + +db-console: ## Access the rails db console + $(RAILS_RUN_CMD) dbconsole + +wait-on-db: + ./bin/wait-for-local-postgres.sh + +################################################## +# Testing +################################################## + +test: ## Run the test suite + $(BUNDLE_EXEC_CMD) rspec --format documentation $(args) + +################################################## +# Lint & Formatting +################################################## + +lint: ## Run the linter with auto-fixing + $(RUBY_RUN_CMD) ./bin/rubocop -a + +lint-ci: ## Run the linter, but don't fix anything + $(RUBY_RUN_CMD) ./bin/rubocop + +################################################## +# Rails +################################################## + +rails-console: + $(RAILS_RUN_CMD) console + +rails-routes: + $(RAILS_RUN_CMD) routes + +rails-generate: + $(RAILS_RUN_CMD) generate $(GENERATE_COMMAND) + +locale: + @:$(call check_defined, MODEL, the name of model to generate) + $(RAILS_RUN_CMD) generate locale $(MODEL) + +################################################## +# Miscellaneous Utilities +################################################## + +help: ## Prints the help documentation and info about each command + @grep -Eh '^[/a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/app-rails/Procfile.dev b/app-rails/Procfile.dev index 852e6c7..5768477 100644 --- a/app-rails/Procfile.dev +++ b/app-rails/Procfile.dev @@ -1,2 +1,2 @@ -web: bin/rails server -p 3000 -css: bin/rails dartsass:watch +web: env RUBY_DEBUG_OPEN=true bin/rails server -b $RAILS_BINDING +css: npm run build:css -- --watch diff --git a/app-rails/app/assets/config/manifest.js b/app-rails/app/assets/config/manifest.js index 4028c22..a13c63b 100644 --- a/app-rails/app/assets/config/manifest.js +++ b/app-rails/app/assets/config/manifest.js @@ -1,4 +1,17 @@ +// Specify assets to be included in the final application. +// Any assets linked here will be compiled by Sprockets into +// the public/assets folder when running assets:precompile. + //= link_tree ../images //= link_tree ../../javascript .js //= link_tree ../../../vendor/javascript .js + +// USWDS assets +// You can reference these relative to the dist directory (e.g. "assets/img" and "assets/js") +//= link_tree ../../../node_modules/@uswds/uswds/dist/img +//= link_tree ../../../node_modules/@uswds/uswds/dist/fonts +//= link @uswds/uswds/dist/js/uswds-init.min.js +//= link @uswds/uswds/dist/js/uswds.min.js + +// Compiled CSS and JS //= link_tree ../builds diff --git a/app-rails/app/assets/stylesheets/_uswds-theme.scss b/app-rails/app/assets/stylesheets/_uswds-theme.scss new file mode 100644 index 0000000..1c8f4f4 --- /dev/null +++ b/app-rails/app/assets/stylesheets/_uswds-theme.scss @@ -0,0 +1,33 @@ +/* +---------------------------------------- +USWDS settings overrides +---------------------------------------- +See https://designsystem.digital.gov/documentation/settings/ +for a full list of available settings. +---------------------------------------- +*/ + +@use "uswds-core" with ( + // Disable scary but mostly irrelevant warnings: + $theme-show-notifications: false, + + // Ensure utility classes always override other styles + $utilities-use-important: true, + + // Use USWDS defaults for typography + $theme-style-body-element: true, + $theme-font-type-sans: "public-sans", + $theme-global-content-styles: true, + $theme-global-link-styles: true, + $theme-global-paragraph-styles: true, + + $theme-h1-font-size: "2xl", + $theme-h2-font-size: "lg", + $theme-h3-font-size: "md", + + $theme-font-weight-semibold: 600, + + // See also: config/initializers/uswds.rb + $theme-image-path: "@uswds/uswds/dist/img", + $theme-font-path: "@uswds/uswds/dist/fonts" +); diff --git a/app-rails/app/assets/stylesheets/application.scss b/app-rails/app/assets/stylesheets/application.scss index 8d7af90..e45e5c5 100644 --- a/app-rails/app/assets/stylesheets/application.scss +++ b/app-rails/app/assets/stylesheets/application.scss @@ -1 +1,46 @@ -// Sassy +@forward "uswds-theme"; +@forward "uswds"; + +@use "uswds-core" as *; + +// ========================================================= +// Base typography +// ========================================================= +h1, +h2, +h3, +h4 { + text-wrap: balance; +} + +.usa-breadcrumb + h1 { + margin-top: units(3); +} + +p, +li, +label, +legend { + text-wrap: pretty; +} + +// ========================================================= +// Custom utilities missing from USWDS (for now) +// ========================================================= + +// Gap - USWDS has grid-gap but it doesn't work with flexbox +@for $i from 0 through 10 { + .gap-#{$i} { + gap: units($i); + } +} + +// ========================================================= +// Developer tooling +// ========================================================= +// This shows when an i18n key is missing a value +.translation_missing { + color: red; + font-weight: bold; + border: 3px dashed red; +} diff --git a/app-rails/app/controllers/application_controller.rb b/app-rails/app/controllers/application_controller.rb index 09705d1..98ba544 100644 --- a/app-rails/app/controllers/application_controller.rb +++ b/app-rails/app/controllers/application_controller.rb @@ -1,2 +1,12 @@ +# Not to be confused with a "benefits application" or "claim". +# This is the parent class for all other controllers in the application. class ApplicationController < ActionController::Base + around_action :switch_locale + + # Set the active locale based on the URL + # For example, if the URL starts with /es-US, the locale will be set to :es-US + def switch_locale(&action) + locale = params[:locale] || I18n.default_locale + I18n.with_locale(locale, &action) + end end diff --git a/app-rails/app/helpers/application_helper.rb b/app-rails/app/helpers/application_helper.rb index de6be79..e7570c2 100644 --- a/app-rails/app/helpers/application_helper.rb +++ b/app-rails/app/helpers/application_helper.rb @@ -1,2 +1,10 @@ module ApplicationHelper + def us_form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block) + options[:builder] = UswdsFormBuilder + form_with model: model, scope: scope, url: url, format: format, **options, &block + end + + def local_time(time, format: nil, timezone: "America/Chicago") + I18n.l(time.in_time_zone(timezone), format: format) + end end diff --git a/app-rails/app/helpers/uswds_form_builder.rb b/app-rails/app/helpers/uswds_form_builder.rb new file mode 100644 index 0000000..8d9ff9b --- /dev/null +++ b/app-rails/app/helpers/uswds_form_builder.rb @@ -0,0 +1,192 @@ +# Custom form builder. Beyond adding USWDS classes, this also +# supports setting the label, hint, and error messages by just +# using the field helpers (i.e text_field, check_box), and adds +# additional helpers like fieldset and hint. +# https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html +class UswdsFormBuilder < ActionView::Helpers::FormBuilder + def initialize(*args) + super + self.options[:html] ||= {} + self.options[:html][:class] ||= "usa-form usa-form--large" + end + + ######################################## + # Override standard helpers + ######################################## + + # Override default text fields to automatically include the label, + # hint, and error elements + # + # Example usage: + # <%= f.text_field :foobar, { label: "Custom label text", hint: "Some hint text" } %> + %i[email_field file_field password_field text_area text_field].each do |field_type| + define_method(field_type) do |attribute, options = {}| + classes = us_class_for_field_type(field_type, options[:width]) + classes += " usa-input--error" if has_error?(attribute) + + options[:class] ||= "" + options[:class].prepend("#{classes} ") + + label_text = options.delete(:label) + + us_form_group(attribute: attribute) do + us_text_field_label(attribute, label_text, options) + super(attribute, options) + end + end + end + + def check_box(attribute, options = {}, *args) + options[:class] ||= "" + options[:class].prepend(us_class_for_field_type(:check_box)) + + label_text = options.delete(:label) + + @template.content_tag(:div, class: "usa-checkbox") do + super(attribute, options, *args) + us_toggle_label("checkbox", attribute, label_text, options) + end + end + + def radio_button(attribute, tag_value, options = {}) + options[:class] ||= "" + options[:class].prepend(us_class_for_field_type(:radio_button)) + + label_text = options.delete(:label) + label_options = { for: field_id(attribute, tag_value) }.merge(options) + + @template.content_tag(:div, class: "usa-radio") do + super(attribute, tag_value, options) + us_toggle_label("radio", attribute, label_text, label_options) + end + end + + def select(attribute, choices, options = {}, html_options = {}) + classes = "usa-select" + + html_options[:class] ||= "" + html_options[:class].prepend("#{classes} ") + + label_text = options.delete(:label) + + us_form_group(attribute: attribute) do + us_text_field_label(attribute, label_text, options) + super(attribute, choices, options, html_options) + end + end + + def submit(value = nil, options = {}) + options[:class] ||= "" + options[:class].prepend("usa-button ") + + super(value, options) + end + + ######################################## + # Custom helpers + ######################################## + + def field_error(attribute) + return unless has_error?(attribute) + + @template.content_tag(:span, object.errors[attribute].to_sentence, class: "usa-error-message") + end + + def fieldset(legend, attribute = nil, &block) + us_form_group(attribute: attribute) do + @template.content_tag(:fieldset, class: "usa-fieldset") do + @template.content_tag(:legend, legend, class: "usa-legend") + @template.capture(&block) + end + end + end + + # Check if a field has a validation error + def has_error?(attribute) + return unless object + object.errors.has_key?(attribute) + end + + def human_name(attribute) + return unless object + object.class.human_attribute_name(attribute) + end + + def hint(text) + @template.content_tag(:div, @template.raw(text), class: "usa-hint") + end + + def yes_no(attribute, options = {}) + yes_options = options[:yes_options] || {} + no_options = options[:no_options] || {} + value = if object then object.send(attribute) else nil end + + yes_options = { label: I18n.t("us_form_with.boolean_true") }.merge(yes_options) + no_options = { label: I18n.t("us_form_with.boolean_false") }.merge(no_options) + + @template.capture do + # Hidden field included for same reason as radio button collections (https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-collection_radio_buttons) + hidden_field(attribute, value: "") + + fieldset(options[:legend] || human_name(attribute), attribute) do + buttons = + radio_button(attribute, true, yes_options) + + radio_button(attribute, false, no_options) + + if has_error?(attribute) + field_error(attribute) + buttons + else + buttons + end + end + end + end + + private + def us_class_for_field_type(field_type, width = nil) + case field_type + when :check_box + "usa-checkbox__input usa-checkbox__input--tile" + when :file_field + "usa-file-input" + when :radio_button + "usa-radio__input usa-radio__input--tile" + when :text_area + "usa-textarea" + else + classes = "usa-input" + classes += " usa-input--#{width}" if width + classes + end + end + + + # Render the label, hint text, and error message for a form field + def us_text_field_label(attribute, text = nil, options = {}) + hint_text = options.delete(:hint) + + if hint_text + hint_id = "#{attribute}_hint" + options[:aria_describedby] = hint_id + hint = @template.content_tag(:div, @template.raw(hint_text), id: hint_id, class: "usa-hint") + end + + label(attribute, text, { class: "usa-label" }) + hint + field_error(attribute) + end + + # Label for a checkbox or radio + def us_toggle_label(type, attribute, text = nil, options = {}) + hint_text = options.delete(:hint) + label_text = text || object.class.human_attribute_name(attribute) + options = options.merge({ class: "usa-#{type}__label" }) + + if hint_text + hint = @template.content_tag(:span, hint_text, class: "usa-#{type}__label-description") + label_text = "#{label_text} #{hint}".html_safe + end + + label(attribute, label_text, options) + end + + def us_form_group(attribute: nil, show_error: nil, &block) + children = @template.capture(&block) + classes = "usa-form-group" + classes += " usa-form-group--error" if show_error or (attribute and has_error?(attribute)) + + @template.content_tag(:div, children, class: classes) + end +end diff --git a/app-rails/app/javascript/application.js b/app-rails/app/javascript/application.js index 0d7b494..1a56151 100644 --- a/app-rails/app/javascript/application.js +++ b/app-rails/app/javascript/application.js @@ -1,3 +1,4 @@ +//= require activestorage // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" import "controllers" diff --git a/app-rails/app/views/layouts/application.html.erb b/app-rails/app/views/layouts/application.html.erb index a80e872..196a22c 100644 --- a/app-rails/app/views/layouts/application.html.erb +++ b/app-rails/app/views/layouts/application.html.erb @@ -1,16 +1,47 @@ +<%# Top-level layout for all pages in the application %> - + - TemplateApplicationRails + + <%= content_for?(:title) ? "#{ yield(:title) } | #{ t('header.title') }" : t("header.title") %> + <%= csrf_meta_tags %> <%= csp_meta_tag %> + <%= favicon_link_tag asset_path('@uswds/uswds/dist/img/us_flag_small.png') %> + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= yield :head %> + <%= javascript_importmap_tags %> + <%= javascript_include_tag '@uswds/uswds/dist/js/uswds-init.min.js' %> - <%= yield %> +
+ <%= render partial: 'header' %> + +
+
+
+
+ <%= render partial: 'flash' %> + +
+ <%= yield :before_content_col %> +
+ <%= content_for?(:content) ? yield(:content) : yield %> +
+ <%= yield :after_content_col %> +
+
+
+
+
+
+ + <%= javascript_include_tag '@uswds/uswds/dist/js/uswds.min.js' %> + <%= yield :scripts %> diff --git a/app-rails/bin/db-migrate b/app-rails/bin/db-migrate new file mode 100755 index 0000000..139f271 --- /dev/null +++ b/app-rails/bin/db-migrate @@ -0,0 +1,9 @@ +#!/bin/sh +echo "Running migrations..." +echo " DB_HOST=$DB_HOST" +echo " DB_PORT=$DB_PORT" +echo " DB_USER=$DB_USER" +echo " DB_NAME=$DB_NAME" +echo " DB_SCHEMA=$DB_SCHEMA" + +./bin/rake --trace db:migrate diff --git a/app-rails/bin/dev b/app-rails/bin/dev index 74ade16..edca718 100755 --- a/app-rails/bin/dev +++ b/app-rails/bin/dev @@ -1,8 +1,16 @@ #!/usr/bin/env sh -if ! gem list foreman -i --silent; then - echo "Installing foreman..." - gem install foreman +if gem list --no-installed --exact --silent overman; then + echo "Installing overman..." + gem install overman fi -exec foreman start -f Procfile.dev "$@" +# Default to port 3000 if not specified +export PORT="${PORT:-3000}" +export RAILS_BINDING="${RAILS_BINDING:-localhost}" + +# -e /dev/null to disable the limited/broken .env file loading logic, which does +# not respect existing env vars +# +# https://github.com/ddollar/foreman/pull/711 +exec overman start -f Procfile.dev -e /dev/null "$@" diff --git a/app-rails/bin/rubocop b/app-rails/bin/rubocop new file mode 100755 index 0000000..369a05b --- /dev/null +++ b/app-rails/bin/rubocop @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rubocop' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rubocop", "rubocop") diff --git a/app-rails/bin/wait-for-local-postgres.sh b/app-rails/bin/wait-for-local-postgres.sh new file mode 100755 index 0000000..20cbbd3 --- /dev/null +++ b/app-rails/bin/wait-for-local-postgres.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# wait-for-local-postgres + +set -e + +# Color formatting +RED='\033[0;31m' +NO_COLOR='\033[0m' + +MAX_WAIT_TIME=30 # seconds +wait_time=0 + +# If you run your DB on a port other than 5432, you'll need to specify +# the port's environment variable you want to use for this to work properly +# "export DB_PORT=" +DB_PORT="${DB_PORT:=5432}" + +# Support other container tools like `finch` +DOCKER_CMD="${CONTAINER_CMD:=docker}" + +# If pg_isready isn't available, the loop would just keep going until it fails +# instead just do a sleep and tell the user to install it. Not as good, but shouldn't +# block developers who are just getting started this way +if ! command -v pg_isready &>/dev/null; then + echo -e "${RED}Warning:${NO_COLOR} Postgres has not been installed locally, cannot use pg_isready to check if DB is available." + echo "" + echo "Please install postgresql:" + echo " MacOS: 'brew install postgresql'" + echo " Linux: 'sudo apt install postgresql-client-14 postgresql-client-common'" + echo "" + echo "Sleeping for 5 seconds instead" + sleep 5 + exit 0 +fi + +# Use pg_isready to wait for the DB to be ready to accept connections +# We check every 3 seconds and consider it failed if it gets to 30+ +# https://www.postgresql.org/docs/current/app-pg-isready.html +until pg_isready -h localhost -p $DB_PORT -d local-postgres-db -q; do + echo "waiting on Postgres DB to initialize..." + sleep 3 + + wait_time=$(($wait_time + 3)) + if [ $wait_time -gt $MAX_WAIT_TIME ]; then + echo -e "${RED}ERROR: Database appears to not be starting up, running \"${DOCKER_CMD} logs main-db\" to troubleshoot${NO_COLOR}" + ${DOCKER_CMD} logs main-db + exit 1 + fi +done + +echo "Postgres DB is ready after ~${wait_time} seconds" diff --git a/app-rails/db/seeds.rb b/app-rails/db/seeds.rb index 4fbd6ed..56bfa67 100644 --- a/app-rails/db/seeds.rb +++ b/app-rails/db/seeds.rb @@ -7,3 +7,15 @@ # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| # MovieGenre.find_or_create_by!(name: genre_name) # end + +# Load seeds per Rails environment. +seed_filename = "#{ENV["SEED_FILENAME"] || Rails.env.downcase}.rb" +seed_path = Rails.root.join('db', 'seeds', seed_filename) + +if File.file?(seed_path) + puts "ðŸŒą Seeding the database with #{seed_filename}" + load(seed_path) + puts "ðŸŠī Done seeding" +else + puts "ðŸŒē No seeding necessary" +end \ No newline at end of file diff --git a/app-rails/db/seeds/.keep b/app-rails/db/seeds/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app-rails/lib/generators/locale/locale_generator.rb b/app-rails/lib/generators/locale/locale_generator.rb new file mode 100644 index 0000000..76c4652 --- /dev/null +++ b/app-rails/lib/generators/locale/locale_generator.rb @@ -0,0 +1,21 @@ +class LocaleGenerator < Rails::Generators::NamedBase + source_root File.expand_path("templates", __dir__) + + def create_model_locale_file + template "model_locale.yml", "config/locales/models/#{file_name}/en.yml" + end + + def create_view_locale_file + template "view_locale.yml", "config/locales/views/#{plural_file_name}/en.yml" + end + + private + + def file_name + name.underscore + end + + def plural_file_name + file_name.pluralize + end +end diff --git a/app-rails/lib/generators/locale/templates/model_locale.yml b/app-rails/lib/generators/locale/templates/model_locale.yml new file mode 100644 index 0000000..09f538b --- /dev/null +++ b/app-rails/lib/generators/locale/templates/model_locale.yml @@ -0,0 +1,4 @@ +en: + activerecord: + attributes: + <%= file_name %>: \ No newline at end of file diff --git a/app-rails/lib/generators/locale/templates/view_locale.yml b/app-rails/lib/generators/locale/templates/view_locale.yml new file mode 100644 index 0000000..6747d09 --- /dev/null +++ b/app-rails/lib/generators/locale/templates/view_locale.yml @@ -0,0 +1,6 @@ +en: + <%= plural_file_name %>: + index: + show: + new: + edit: \ No newline at end of file diff --git a/app-rails/local.env.example b/app-rails/local.env.example new file mode 100644 index 0000000..2cc2456 --- /dev/null +++ b/app-rails/local.env.example @@ -0,0 +1,21 @@ +############################ +# Rails +############################ + +RAILS_ENV=development + +############################ +# Rails: Urls +############################ + +APP_HOST=localhost +APP_PORT=3000 + +############################ +# Database +############################ + +DB_HOST=127.0.0.1 +DB_NAME=app +DB_USER=app +DB_PASSWORD=secret123 diff --git a/app-rails/package-lock.json b/app-rails/package-lock.json new file mode 100644 index 0000000..06be560 --- /dev/null +++ b/app-rails/package-lock.json @@ -0,0 +1,500 @@ +{ + "name": "pfml", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "pfml", + "version": "1.0.0", + "dependencies": { + "@uswds/uswds": "^3.7.1" + }, + "devDependencies": { + "sass": "^1.71.1" + } + }, + "node_modules/@uswds/uswds": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.8.0.tgz", + "integrity": "sha512-rMwCXe/u4HGkfskvS1Iuabapi/EXku3ChaIFW7y/dUhc7R1TXQhbbfp8YXEjmXPF0yqJnv9T08WPgS0fQqWZ8w==", + "dependencies": { + "classlist-polyfill": "1.2.0", + "object-assign": "4.1.1", + "receptor": "1.0.0", + "resolve-id-refs": "0.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/classlist-polyfill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz", + "integrity": "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==" + }, + "node_modules/element-closest": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/element-closest/-/element-closest-2.0.2.tgz", + "integrity": "sha512-QCqAWP3kwj8Gz9UXncVXQGdrhnWxD8SQBSeZp5pOsyCcQ6RpL738L1/tfuwBiMi6F1fYkxqPnBrFBR4L+f49Cg==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/keyboardevent-key-polyfill": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keyboardevent-key-polyfill/-/keyboardevent-key-polyfill-1.1.0.tgz", + "integrity": "sha512-NTDqo7XhzL1fqmUzYroiyK2qGua7sOMzLav35BfNA/mPUSCtw8pZghHFMTYR9JdnJ23IQz695FcaM6EE6bpbFQ==" + }, + "node_modules/matches-selector": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/matches-selector/-/matches-selector-1.2.0.tgz", + "integrity": "sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/receptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/receptor/-/receptor-1.0.0.tgz", + "integrity": "sha512-yvVEqVQDNzEmGkluCkEdbKSXqZb3WGxotI/VukXIQ+4/BXEeXVjWtmC6jWaR1BIsmEAGYQy3OTaNgDj2Svr01w==", + "dependencies": { + "element-closest": "^2.0.1", + "keyboardevent-key-polyfill": "^1.0.2", + "matches-selector": "^1.0.0", + "object-assign": "^4.1.0" + } + }, + "node_modules/resolve-id-refs": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/resolve-id-refs/-/resolve-id-refs-0.1.0.tgz", + "integrity": "sha512-hNS03NEmVpJheF7yfyagNh57XuKc0z+NkSO0oBbeO67o6IJKoqlDfnNIxhjp7aTWwjmSWZQhtiGrOgZXVyM90w==" + }, + "node_modules/sass": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.72.0.tgz", + "integrity": "sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + } + }, + "dependencies": { + "@uswds/uswds": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.8.0.tgz", + "integrity": "sha512-rMwCXe/u4HGkfskvS1Iuabapi/EXku3ChaIFW7y/dUhc7R1TXQhbbfp8YXEjmXPF0yqJnv9T08WPgS0fQqWZ8w==", + "requires": { + "classlist-polyfill": "1.2.0", + "object-assign": "4.1.1", + "receptor": "1.0.0", + "resolve-id-refs": "0.1.0" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "classlist-polyfill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz", + "integrity": "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==" + }, + "element-closest": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/element-closest/-/element-closest-2.0.2.tgz", + "integrity": "sha512-QCqAWP3kwj8Gz9UXncVXQGdrhnWxD8SQBSeZp5pOsyCcQ6RpL738L1/tfuwBiMi6F1fYkxqPnBrFBR4L+f49Cg==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "keyboardevent-key-polyfill": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keyboardevent-key-polyfill/-/keyboardevent-key-polyfill-1.1.0.tgz", + "integrity": "sha512-NTDqo7XhzL1fqmUzYroiyK2qGua7sOMzLav35BfNA/mPUSCtw8pZghHFMTYR9JdnJ23IQz695FcaM6EE6bpbFQ==" + }, + "matches-selector": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/matches-selector/-/matches-selector-1.2.0.tgz", + "integrity": "sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "receptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/receptor/-/receptor-1.0.0.tgz", + "integrity": "sha512-yvVEqVQDNzEmGkluCkEdbKSXqZb3WGxotI/VukXIQ+4/BXEeXVjWtmC6jWaR1BIsmEAGYQy3OTaNgDj2Svr01w==", + "requires": { + "element-closest": "^2.0.1", + "keyboardevent-key-polyfill": "^1.0.2", + "matches-selector": "^1.0.0", + "object-assign": "^4.1.0" + } + }, + "resolve-id-refs": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/resolve-id-refs/-/resolve-id-refs-0.1.0.tgz", + "integrity": "sha512-hNS03NEmVpJheF7yfyagNh57XuKc0z+NkSO0oBbeO67o6IJKoqlDfnNIxhjp7aTWwjmSWZQhtiGrOgZXVyM90w==" + }, + "sass": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.72.0.tgz", + "integrity": "sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } +} diff --git a/app-rails/package.json b/app-rails/package.json new file mode 100644 index 0000000..c9528af --- /dev/null +++ b/app-rails/package.json @@ -0,0 +1,14 @@ +{ + "name": "pfml", + "private": true, + "version": "1.0.0", + "scripts": { + "build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css ./app/assets/stylesheets/admin.scss:./app/assets/builds/admin.css --load-path=node_modules --load-path=node_modules/@uswds/uswds/packages" + }, + "dependencies": { + "@uswds/uswds": "^3.7.1" + }, + "devDependencies": { + "sass": "^1.71.1" + } +} From 5c3102916326a3e098bcbd71bdc5c19bf01f2505 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 9 Apr 2024 14:53:25 -0700 Subject: [PATCH 04/76] Add environment configuration --- app-rails/config/environments/development.rb | 6 ++++++ app-rails/config/environments/production.rb | 3 +++ app-rails/config/environments/test.rb | 3 +++ 3 files changed, 12 insertions(+) diff --git a/app-rails/config/environments/development.rb b/app-rails/config/environments/development.rb index 2e7fb48..f2aa3d1 100644 --- a/app-rails/config/environments/development.rb +++ b/app-rails/config/environments/development.rb @@ -1,5 +1,8 @@ require "active_support/core_ext/integer/time" +# Custom setting: set the default url. +Rails.application.default_url_options = { host: ENV["APP_HOST"], port: ENV["APP_PORT"]} + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -36,6 +39,9 @@ # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local + # Custom setting: Use letter opener gem for local mail delivery. + config.action_mailer.delivery_method = :letter_opener + # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false diff --git a/app-rails/config/environments/production.rb b/app-rails/config/environments/production.rb index bc10944..7acdab9 100644 --- a/app-rails/config/environments/production.rb +++ b/app-rails/config/environments/production.rb @@ -1,5 +1,8 @@ require "active_support/core_ext/integer/time" +# Custom setting: set the default url. +Rails.application.default_url_options = { host: ENV["APP_HOST"], port: ENV["APP_PORT"]} + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. diff --git a/app-rails/config/environments/test.rb b/app-rails/config/environments/test.rb index adbb4a6..a57a0f1 100644 --- a/app-rails/config/environments/test.rb +++ b/app-rails/config/environments/test.rb @@ -5,6 +5,9 @@ # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! +# Custom setting: set the default url. +Rails.application.default_url_options[:host] = "localhost" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. From 398dfd998ea968343cbde3975284a40e59097b73 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 9 Apr 2024 14:53:47 -0700 Subject: [PATCH 05/76] Add USWDS config --- app-rails/config/initializers/assets.rb | 2 ++ app-rails/config/initializers/uswds.rb | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 app-rails/config/initializers/uswds.rb diff --git a/app-rails/config/initializers/assets.rb b/app-rails/config/initializers/assets.rb index 2eeef96..f0e0ae0 100644 --- a/app-rails/config/initializers/assets.rb +++ b/app-rails/config/initializers/assets.rb @@ -5,8 +5,10 @@ # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path +Rails.application.config.assets.paths << Rails.root.join("node_modules") # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) +Rails.application.config.assets.paths << Rails.root.join("node_modules/@fortawesome/fontawesome-free/webfonts") diff --git a/app-rails/config/initializers/uswds.rb b/app-rails/config/initializers/uswds.rb new file mode 100644 index 0000000..f49c8ea --- /dev/null +++ b/app-rails/config/initializers/uswds.rb @@ -0,0 +1,9 @@ +# Ensure the USWDS dist folder is compiled as part of assets. +# +# This serves a couple different purposes: +# - Ensures that images and fonts are exposed properly on the client-side, +# since USWDS CSS references specific assets for their design components. +# - Ensures that the js files are exposed properly for use in the application layout. +# +# See also: app/assets/stylesheets/_uswds-theme.scss +Rails.application.config.assets.paths << Rails.root.join("node_modules/@uswds/uswds/dist") From 45bcab728af79cc3833c094180966f881505e466 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 9 Apr 2024 14:53:58 -0700 Subject: [PATCH 06/76] Add ActiveStorage callback hooks --- .../active_storage_attachment_callbacks.rb | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 app-rails/config/initializers/active_storage_attachment_callbacks.rb diff --git a/app-rails/config/initializers/active_storage_attachment_callbacks.rb b/app-rails/config/initializers/active_storage_attachment_callbacks.rb new file mode 100644 index 0000000..2e79919 --- /dev/null +++ b/app-rails/config/initializers/active_storage_attachment_callbacks.rb @@ -0,0 +1,37 @@ +# If a model uses Active Storage and it defines any of the defined methods (after_commit, +# after_create_commit, or after_update_commit), then those methods will be called after +# the attachment has been fully committed to database. This allows for post-processing +# after uploads, since uploads are not immediately available. +# +# ðŸŽĐ Hat tip and thanks to: +# - https://redgreen.no/2021/01/25/active-storage-callbacks.html +# - https://stackoverflow.com/questions/53226228/callback-for-active-storage-file-upload +Rails.configuration.to_prepare do + module ActiveStorage::Attachment::Callbacks + # Gives us some convenient shortcuts, like `prepended` + extend ActiveSupport::Concern + + # When prepended into a class, define our callback + prepended do + after_commit :after_commit + after_create_commit :after_create_commit + after_update_commit :after_update_commit + end + + # Callback methods + def after_commit + record.after_attachment_commit(self) if record.respond_to? :after_attachment_commit + end + + def after_create_commit + record.after_attachment_create_commit(self) if record.respond_to? :after_attachment_create_commit + end + + def after_update_commit + record.after_attachment_update_commit(self) if record.respond_to? :after_attachment_update_commit + end + end + + # After defining the module, call on ActiveStorage::Attachment to prepend it in. + ActiveStorage::Attachment.prepend ActiveStorage::Attachment::Callbacks +end From bb4faeedc48f9ed172fe89cdaa78d01f839b20ae Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 9 Apr 2024 16:02:46 -0700 Subject: [PATCH 07/76] Start adding AWS integrations --- app-rails/.gitignore | 1 + app-rails/Gemfile | 11 ++++++ app-rails/Gemfile.lock | 65 ++++++++++++++++++++++++++++++++++++ app-rails/Makefile | 4 +++ app-rails/config/storage.yml | 5 +++ app-rails/local.env.example | 19 ++++++++++- 6 files changed, 104 insertions(+), 1 deletion(-) diff --git a/app-rails/.gitignore b/app-rails/.gitignore index 9f92f00..43d4f3b 100644 --- a/app-rails/.gitignore +++ b/app-rails/.gitignore @@ -38,6 +38,7 @@ # Ignore master key for decrypting credentials and more. /config/master.key +/config/credentials/* # Ignore development log. /log/development.log diff --git a/app-rails/Gemfile b/app-rails/Gemfile index 6b28c31..a844d48 100644 --- a/app-rails/Gemfile +++ b/app-rails/Gemfile @@ -50,7 +50,18 @@ gem "bootsnap", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" +# Authentication and Authorization +gem "aws-sdk-cognitoidentityprovider", "~> 1.88" +gem "devise" +gem "jwt" +gem "pundit" +gem "pundit-matchers" + +# Notifications: AWS SES +gem "aws-sdk-rails" + # File uploads: AWS S3 +gem "aws-sdk-s3", require: false gem "active_storage_validations" # HTTP client for external API calls diff --git a/app-rails/Gemfile.lock b/app-rails/Gemfile.lock index 65b83f4..1516e62 100644 --- a/app-rails/Gemfile.lock +++ b/app-rails/Gemfile.lock @@ -85,17 +85,54 @@ GEM ast (2.4.2) aws-eventstream (1.3.0) aws-partitions (1.905.0) + aws-record (2.13.0) + aws-sdk-dynamodb (~> 1, >= 1.85.0) + aws-sdk-cognitoidentityprovider (1.88.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) aws-sdk-core (3.191.5) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) + aws-sdk-dynamodb (1.106.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) + aws-sdk-kms (1.78.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) + aws-sdk-rails (3.12.0) + aws-record (~> 2) + aws-sdk-ses (~> 1, >= 1.50.0) + aws-sdk-sesv2 (~> 1, >= 1.34.0) + aws-sdk-sqs (~> 1, >= 1.56.0) + aws-sessionstore-dynamodb (~> 2) + concurrent-ruby (~> 1) + railties (>= 5.2.0) aws-sdk-rds (1.223.0) aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.146.1) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.8) + aws-sdk-ses (1.59.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) + aws-sdk-sesv2 (1.45.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) + aws-sdk-sqs (1.70.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) + aws-sessionstore-dynamodb (2.2.0) + aws-sdk-dynamodb (~> 1, >= 1.85.0) + rack (>= 2, < 4) + rack-session (>= 1, < 3) aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) base64 (0.2.0) + bcrypt (3.1.20) bigdecimal (3.1.7) bindex (0.8.1) bootsnap (1.18.3) @@ -121,6 +158,12 @@ GEM debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) + devise (4.9.3) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) diff-lcs (1.5.1) dotenv (3.1.0) drb (2.2.1) @@ -153,6 +196,8 @@ GEM activesupport (>= 5.0.0) jmespath (1.6.2) json (2.7.1) + jwt (2.8.1) + base64 language_server-protocol (3.17.0.3) launchy (3.0.0) addressable (~> 2.8) @@ -197,6 +242,7 @@ GEM racc (~> 1.4) nokogiri (1.16.3-x86_64-linux) racc (~> 1.4) + orm_adapter (0.5.0) parallel (1.24.0) parser (3.3.0.5) ast (~> 2.4.1) @@ -210,6 +256,13 @@ GEM public_suffix (5.0.4) puma (6.4.2) nio4r (~> 2.0) + pundit (2.3.1) + activesupport (>= 3.0.0) + pundit-matchers (3.1.2) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) racc (1.7.3) rack (3.0.10) rack-session (2.0.0) @@ -260,6 +313,9 @@ GEM regexp_parser (2.9.0) reline (0.5.0) io-console (~> 0.5) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) rexml (3.2.6) route_translator (14.1.1) actionpack (>= 6.1, < 7.2) @@ -350,6 +406,8 @@ GEM concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) uri (0.13.0) + warden (1.2.9) + rack (>= 2.0.9) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -382,20 +440,27 @@ PLATFORMS DEPENDENCIES active_storage_validations + aws-sdk-cognitoidentityprovider (~> 1.88) + aws-sdk-rails + aws-sdk-s3 bootsnap capybara cssbundling-rails (~> 1.4) debug + devise dotenv factory_bot_rails faker (~> 3.2) faraday importmap-rails jbuilder + jwt letter_opener pg (~> 1.1) pg-aws_rds_iam (~> 0.5.0) puma (>= 5.0) + pundit + pundit-matchers rails (~> 7.1.3, >= 7.1.3.2) rails-erd route_translator diff --git a/app-rails/Makefile b/app-rails/Makefile index e454685..025603d 100644 --- a/app-rails/Makefile +++ b/app-rails/Makefile @@ -194,6 +194,10 @@ locale: @:$(call check_defined, MODEL, the name of model to generate) $(RAILS_RUN_CMD) generate locale $(MODEL) +new-authz-policy: + @:$(call check_defined, MODEL, the name of model to generate) + $(RAILS_RUN_CMD) generate pundit:policy $(MODEL) + ################################################## # Miscellaneous Utilities ################################################## diff --git a/app-rails/config/storage.yml b/app-rails/config/storage.yml index 4942ab6..04f68c1 100644 --- a/app-rails/config/storage.yml +++ b/app-rails/config/storage.yml @@ -6,6 +6,11 @@ local: service: Disk root: <%= Rails.root.join("storage") %> +amazon: + service: S3 + bucket: <%= ENV.fetch("BUCKET_NAME") { nil } %> + + # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 diff --git a/app-rails/local.env.example b/app-rails/local.env.example index 2cc2456..c7749c2 100644 --- a/app-rails/local.env.example +++ b/app-rails/local.env.example @@ -11,6 +11,23 @@ RAILS_ENV=development APP_HOST=localhost APP_PORT=3000 +############################ +# AWS services +############################ + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +BUCKET_NAME=pfml-starter-pfml-dev + +############################ +# Auth +############################ + +COGNITO_CLIENT_SECRET= +COGNITO_USER_POOL_ID=us-east-1_o1btuFuDS +COGNITO_CLIENT_ID=35nrqeng2itarutlqh53tpsv3t + ############################ # Database ############################ @@ -18,4 +35,4 @@ APP_PORT=3000 DB_HOST=127.0.0.1 DB_NAME=app DB_USER=app -DB_PASSWORD=secret123 +DB_PASSWORD=secret123 \ No newline at end of file From 6e9214db0cb3c12995f18bf5c2c60fb69d787aaf Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 9 Apr 2024 16:47:38 -0700 Subject: [PATCH 08/76] Add additional starting setup --- app-rails/.gitignore | 3 + app-rails/.rubocop.yml | 3 + app-rails/Gemfile | 9 +- app-rails/Gemfile.lock | 16 +- app-rails/Makefile | 6 +- app-rails/app/assets/config/manifest.js | 1 + .../app/controllers/application_controller.rb | 22 ++ app-rails/app/controllers/dev_controller.rb | 29 ++ app-rails/app/controllers/home_controller.rb | 10 + .../controllers/users/accounts_controller.rb | 36 +++ .../app/controllers/users/mfa_controller.rb | 72 +++++ .../controllers/users/passwords_controller.rb | 64 ++++ .../users/registrations_controller.rb | 86 +++++ .../controllers/users/sessions_controller.rb | 108 +++++++ .../controllers/registrations_controller.js | 13 + app-rails/app/mailers/application_mailer.rb | 2 +- .../devise/models/cognito_authenticatable.rb | 27 ++ app-rails/app/models/user.rb | 40 +++ app-rails/app/models/user_role.rb | 8 + .../app/views/layouts/employers.html.erb | 23 ++ app-rails/app/views/layouts/users.html.erb | 10 + app-rails/bin/rspec | 7 + app-rails/config/application.rb | 30 +- app-rails/config/database.yml | 15 +- app-rails/config/environments/development.rb | 10 +- app-rails/config/environments/production.rb | 5 +- app-rails/config/environments/test.rb | 2 +- app-rails/config/initializers/devise.rb | 301 ++++++++++++++++++ app-rails/config/locales/defaults/en.yml | 59 ++++ app-rails/config/locales/defaults/es-US.yml | 17 + app-rails/config/locales/devise.en.yml | 65 ++++ app-rails/config/locales/en.yml | 60 ++-- app-rails/config/locales/models/user/en.yml | 5 + .../config/locales/views/application/en.yml | 41 +++ .../locales/views/application/es-US.yml | 5 + app-rails/config/locales/views/home/en.yml | 12 + app-rails/config/locales/views/users/en.yml | 77 +++++ app-rails/config/routes.rb | 48 ++- pfml/app/adapters/auth/cognito_adapter.rb | 292 +++++++++++++++++ pfml/app/adapters/auth/errors.rb | 51 +++ pfml/app/adapters/auth/mock_adapter.rb | 92 ++++++ pfml/app/forms/users/associate_mfa_form.rb | 9 + pfml/app/forms/users/auth_app_code_form.rb | 9 + pfml/app/forms/users/forgot_password_form.rb | 7 + pfml/app/forms/users/mfa_preference_form.rb | 10 + pfml/app/forms/users/new_session_form.rb | 10 + pfml/app/forms/users/registration_form.rb | 12 + .../forms/users/resend_verification_form.rb | 9 + pfml/app/forms/users/reset_password_form.rb | 11 + pfml/app/forms/users/update_email_form.rb | 9 + pfml/app/forms/users/verify_account_form.rb | 11 + pfml/app/helpers/users/mfa_helper.rb | 17 + pfml/app/policies/application_policy.rb | 57 ++++ pfml/app/services/auth_service.rb | 109 +++++++ .../views/application/_breadcrumbs.html.erb | 15 + pfml/app/views/application/_flash.html.erb | 45 +++ pfml/app/views/application/_header.html.erb | 40 +++ .../application/_language-toggle.html.erb | 11 + pfml/app/views/application/sandbox.html.erb | 44 +++ pfml/app/views/home/index.html.erb | 72 +++++ pfml/app/views/users/accounts/edit.html.erb | 35 ++ pfml/app/views/users/mfa/new.html.erb | 50 +++ pfml/app/views/users/mfa/preference.html.erb | 12 + .../app/views/users/passwords/forgot.html.erb | 10 + pfml/app/views/users/passwords/reset.html.erb | 31 ++ .../views/users/registrations/new.html.erb | 64 ++++ .../new_account_verification.html.erb | 26 ++ .../views/users/sessions/challenge.html.erb | 11 + pfml/app/views/users/sessions/new.html.erb | 34 ++ pfml/spec/adapters/cognito_adapter_spec.rb | 153 +++++++++ pfml/spec/controllers/home_controller_spec.rb | 13 + .../users/accounts_controller_spec.rb | 68 ++++ .../controllers/users/mfa_controller_spec.rb | 136 ++++++++ .../users/passwords_controller_spec.rb | 99 ++++++ .../users/registrations_controller_spec.rb | 121 +++++++ .../users/sessions_controller_spec.rb | 144 +++++++++ pfml/spec/factories/user_role_factory.rb | 18 ++ pfml/spec/factories/users_factory.rb | 20 ++ .../spec/forms/users/new_session_form_spec.rb | 32 ++ .../forms/users/registration_form_spec.rb | 39 +++ .../forms/users/verify_account_form_spec.rb | 31 ++ pfml/spec/models/user_spec.rb | 71 +++++ pfml/spec/rails_helper.rb | 74 +++++ pfml/spec/services/auth_service_spec.rb | 62 ++++ pfml/spec/spec_helper.rb | 96 ++++++ pfml/spec/support/factory_bot.rb | 3 + 86 files changed, 3693 insertions(+), 49 deletions(-) create mode 100644 app-rails/app/controllers/dev_controller.rb create mode 100644 app-rails/app/controllers/home_controller.rb create mode 100644 app-rails/app/controllers/users/accounts_controller.rb create mode 100644 app-rails/app/controllers/users/mfa_controller.rb create mode 100644 app-rails/app/controllers/users/passwords_controller.rb create mode 100644 app-rails/app/controllers/users/registrations_controller.rb create mode 100644 app-rails/app/controllers/users/sessions_controller.rb create mode 100644 app-rails/app/javascript/controllers/registrations_controller.js create mode 100644 app-rails/app/models/concerns/devise/models/cognito_authenticatable.rb create mode 100644 app-rails/app/models/user.rb create mode 100644 app-rails/app/models/user_role.rb create mode 100644 app-rails/app/views/layouts/employers.html.erb create mode 100644 app-rails/app/views/layouts/users.html.erb create mode 100755 app-rails/bin/rspec create mode 100644 app-rails/config/initializers/devise.rb create mode 100644 app-rails/config/locales/defaults/en.yml create mode 100644 app-rails/config/locales/defaults/es-US.yml create mode 100644 app-rails/config/locales/devise.en.yml create mode 100644 app-rails/config/locales/models/user/en.yml create mode 100644 app-rails/config/locales/views/application/en.yml create mode 100644 app-rails/config/locales/views/application/es-US.yml create mode 100644 app-rails/config/locales/views/home/en.yml create mode 100644 app-rails/config/locales/views/users/en.yml create mode 100644 pfml/app/adapters/auth/cognito_adapter.rb create mode 100644 pfml/app/adapters/auth/errors.rb create mode 100644 pfml/app/adapters/auth/mock_adapter.rb create mode 100644 pfml/app/forms/users/associate_mfa_form.rb create mode 100644 pfml/app/forms/users/auth_app_code_form.rb create mode 100644 pfml/app/forms/users/forgot_password_form.rb create mode 100644 pfml/app/forms/users/mfa_preference_form.rb create mode 100644 pfml/app/forms/users/new_session_form.rb create mode 100644 pfml/app/forms/users/registration_form.rb create mode 100644 pfml/app/forms/users/resend_verification_form.rb create mode 100644 pfml/app/forms/users/reset_password_form.rb create mode 100644 pfml/app/forms/users/update_email_form.rb create mode 100644 pfml/app/forms/users/verify_account_form.rb create mode 100644 pfml/app/helpers/users/mfa_helper.rb create mode 100644 pfml/app/policies/application_policy.rb create mode 100644 pfml/app/services/auth_service.rb create mode 100644 pfml/app/views/application/_breadcrumbs.html.erb create mode 100644 pfml/app/views/application/_flash.html.erb create mode 100644 pfml/app/views/application/_header.html.erb create mode 100644 pfml/app/views/application/_language-toggle.html.erb create mode 100644 pfml/app/views/application/sandbox.html.erb create mode 100644 pfml/app/views/home/index.html.erb create mode 100644 pfml/app/views/users/accounts/edit.html.erb create mode 100644 pfml/app/views/users/mfa/new.html.erb create mode 100644 pfml/app/views/users/mfa/preference.html.erb create mode 100644 pfml/app/views/users/passwords/forgot.html.erb create mode 100644 pfml/app/views/users/passwords/reset.html.erb create mode 100644 pfml/app/views/users/registrations/new.html.erb create mode 100644 pfml/app/views/users/registrations/new_account_verification.html.erb create mode 100644 pfml/app/views/users/sessions/challenge.html.erb create mode 100644 pfml/app/views/users/sessions/new.html.erb create mode 100644 pfml/spec/adapters/cognito_adapter_spec.rb create mode 100644 pfml/spec/controllers/home_controller_spec.rb create mode 100644 pfml/spec/controllers/users/accounts_controller_spec.rb create mode 100644 pfml/spec/controllers/users/mfa_controller_spec.rb create mode 100644 pfml/spec/controllers/users/passwords_controller_spec.rb create mode 100644 pfml/spec/controllers/users/registrations_controller_spec.rb create mode 100644 pfml/spec/controllers/users/sessions_controller_spec.rb create mode 100644 pfml/spec/factories/user_role_factory.rb create mode 100644 pfml/spec/factories/users_factory.rb create mode 100644 pfml/spec/forms/users/new_session_form_spec.rb create mode 100644 pfml/spec/forms/users/registration_form_spec.rb create mode 100644 pfml/spec/forms/users/verify_account_form_spec.rb create mode 100644 pfml/spec/models/user_spec.rb create mode 100644 pfml/spec/rails_helper.rb create mode 100644 pfml/spec/services/auth_service_spec.rb create mode 100644 pfml/spec/spec_helper.rb create mode 100644 pfml/spec/support/factory_bot.rb diff --git a/app-rails/.gitignore b/app-rails/.gitignore index 43d4f3b..bd7e1a0 100644 --- a/app-rails/.gitignore +++ b/app-rails/.gitignore @@ -48,3 +48,6 @@ /node_modules/* !/node_modules/.keep + +# Testing +coverage/* diff --git a/app-rails/.rubocop.yml b/app-rails/.rubocop.yml index 64099d0..0705ac1 100644 --- a/app-rails/.rubocop.yml +++ b/app-rails/.rubocop.yml @@ -3,3 +3,6 @@ require: inherit_gem: pundit: config/rubocop-rspec.yml rubocop-rails-omakase: rubocop.yml +AllCops: + Exclude: + - lib/templates/**/* diff --git a/app-rails/Gemfile b/app-rails/Gemfile index a844d48..fc07ca1 100644 --- a/app-rails/Gemfile +++ b/app-rails/Gemfile @@ -21,7 +21,7 @@ gem "cssbundling-rails", "~> 1.4" gem "importmap-rails" # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] -gem "turbo-rails" +gem "turbo-rails", ">= 2.0" # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] gem "stimulus-rails" @@ -56,6 +56,7 @@ gem "devise" gem "jwt" gem "pundit" gem "pundit-matchers" +gem "rqrcode" # Notifications: AWS SES gem "aws-sdk-rails" @@ -81,8 +82,6 @@ group :development, :test do gem "factory_bot_rails" gem "rspec-rails", "~> 6.1.0" - # Generate ERDs - gem "rails-erd" end group :development do @@ -100,12 +99,16 @@ group :development do # Speed up commands on slow machines / big apps [https://github.com/rails/spring] # gem "spring" + + # Generate ERDs + gem "rails-erd" end group :test do # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] gem "capybara" gem "selenium-webdriver" + gem "simplecov", require: false end group :production do diff --git a/app-rails/Gemfile.lock b/app-rails/Gemfile.lock index 1516e62..ac9e5db 100644 --- a/app-rails/Gemfile.lock +++ b/app-rails/Gemfile.lock @@ -149,6 +149,7 @@ GEM xpath (~> 3.2) childprocess (5.0.0) choice (0.2.0) + chunky_png (1.4.0) concurrent-ruby (1.2.3) connection_pool (2.4.1) crass (1.0.6) @@ -165,6 +166,7 @@ GEM responders warden (~> 1.2.3) diff-lcs (1.5.1) + docile (1.4.0) dotenv (3.1.0) drb (2.2.1) erubi (1.12.0) @@ -320,6 +322,10 @@ GEM route_translator (14.1.1) actionpack (>= 6.1, < 7.2) activesupport (>= 6.1, < 7.2) + rqrcode (2.2.0) + chunky_png (~> 1.0) + rqrcode_core (~> 1.0) + rqrcode_core (1.2.0) rspec-core (3.13.0) rspec-support (~> 3.13.0) rspec-expectations (3.13.0) @@ -386,6 +392,12 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.4) sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) @@ -464,13 +476,15 @@ DEPENDENCIES rails (~> 7.1.3, >= 7.1.3.2) rails-erd route_translator + rqrcode rspec-rails (~> 6.1.0) rubocop-rails-omakase rubocop-rspec selenium-webdriver + simplecov sprockets-rails stimulus-rails - turbo-rails + turbo-rails (>= 2.0) tzinfo-data web-console diff --git a/app-rails/Makefile b/app-rails/Makefile index 025603d..ad92f7a 100644 --- a/app-rails/Makefile +++ b/app-rails/Makefile @@ -165,7 +165,7 @@ wait-on-db: ################################################## test: ## Run the test suite - $(BUNDLE_EXEC_CMD) rspec --format documentation $(args) + $(RUBY_RUN_CMD) ./bin/rspec ################################################## # Lint & Formatting @@ -190,6 +190,10 @@ rails-routes: rails-generate: $(RAILS_RUN_CMD) generate $(GENERATE_COMMAND) +clear-cache: + $(RAILS_RUN_CMD) tmp:clear + $(RAILS_RUN_CMD) assets:clean + locale: @:$(call check_defined, MODEL, the name of model to generate) $(RAILS_RUN_CMD) generate locale $(MODEL) diff --git a/app-rails/app/assets/config/manifest.js b/app-rails/app/assets/config/manifest.js index a13c63b..d454c4c 100644 --- a/app-rails/app/assets/config/manifest.js +++ b/app-rails/app/assets/config/manifest.js @@ -15,3 +15,4 @@ // Compiled CSS and JS //= link_tree ../builds +//= link application.css diff --git a/app-rails/app/controllers/application_controller.rb b/app-rails/app/controllers/application_controller.rb index 98ba544..2ac6cc7 100644 --- a/app-rails/app/controllers/application_controller.rb +++ b/app-rails/app/controllers/application_controller.rb @@ -1,7 +1,11 @@ # Not to be confused with a "benefits application" or "claim". # This is the parent class for all other controllers in the application. class ApplicationController < ActionController::Base + include Pundit::Authorization + around_action :switch_locale + after_action :verify_authorized, except: :index + after_action :verify_policy_scoped, only: :index # Set the active locale based on the URL # For example, if the URL starts with /es-US, the locale will be set to :es-US @@ -9,4 +13,22 @@ def switch_locale(&action) locale = params[:locale] || I18n.default_locale I18n.with_locale(locale, &action) end + + # After a user signs in, Devise uses this method to determine where to route them + def after_sign_in_path_for(resource) + unless resource.is_a?(User) + raise "Unexpected resource type" + end + + if resource.mfa_preference.nil? + return users_mfa_preference_path + end + + if resource.employer? + return employers_path + end + + # @TODO: Route to a claimant landing page + users_account_path + end end diff --git a/app-rails/app/controllers/dev_controller.rb b/app-rails/app/controllers/dev_controller.rb new file mode 100644 index 0000000..6a12abe --- /dev/null +++ b/app-rails/app/controllers/dev_controller.rb @@ -0,0 +1,29 @@ +class DevController < ApplicationController + before_action :check_env + + # Frontend sandbox for testing purposes during local development + def sandbox + end + + # Trigger an email to the email param + def send_email + email = params[:email] + + if email.present? + RequestForInformationMailer.with(email_address: email, name: "Anton Weis").task_opened.deliver_now + flash[:notice] = "Email sent to #{email}" + else + flash[:error] = "No email provided" + end + redirect_to dev_sandbox_path + end + + private + + def check_env + unless Rails.env.development? + redirect_to root_path + nil + end + end +end diff --git a/app-rails/app/controllers/home_controller.rb b/app-rails/app/controllers/home_controller.rb new file mode 100644 index 0000000..c5c29ff --- /dev/null +++ b/app-rails/app/controllers/home_controller.rb @@ -0,0 +1,10 @@ +class HomeController < ApplicationController + skip_after_action :verify_authorized + skip_after_action :verify_policy_scoped + + def index + if current_user + redirect_to after_sign_in_path_for(current_user) + end + end +end diff --git a/app-rails/app/controllers/users/accounts_controller.rb b/app-rails/app/controllers/users/accounts_controller.rb new file mode 100644 index 0000000..fcc8345 --- /dev/null +++ b/app-rails/app/controllers/users/accounts_controller.rb @@ -0,0 +1,36 @@ +class Users::AccountsController < ApplicationController + before_action :authenticate_user! + skip_after_action :verify_authorized + + def edit + @email_form = Users::UpdateEmailForm.new({ email: current_user.email }) + @password_form = Users::ForgotPasswordForm.new({ email: current_user.email }) + end + + def update_email + @email_form = Users::UpdateEmailForm.new(user_email_params) + + if @email_form.invalid? + flash.now[:errors] = @email_form.errors.full_messages + return render :edit, status: :unprocessable_entity + end + + begin + auth_service.change_email(current_user.uid, @email_form.email) + rescue Auth::Errors::BaseAuthError => e + flash.now[:errors] = [ e.message ] + return render :edit, status: :unprocessable_entity + end + + redirect_to({ action: :edit }, notice: "Account updated successfully.") + end + + private + def auth_service + AuthService.new + end + + def user_email_params + params.require(:users_update_email_form).permit(:email) + end +end diff --git a/app-rails/app/controllers/users/mfa_controller.rb b/app-rails/app/controllers/users/mfa_controller.rb new file mode 100644 index 0000000..615db45 --- /dev/null +++ b/app-rails/app/controllers/users/mfa_controller.rb @@ -0,0 +1,72 @@ +# https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-totp.html#totp-mfa-set-up-api +class Users::MfaController < ApplicationController + before_action :authenticate_user! + skip_after_action :verify_authorized + + # Initial page a user is shown after creating an account + def preference + @form = Users::MfaPreferenceForm.new + end + + def update_preference + @form = Users::MfaPreferenceForm.new(params.fetch(:users_mfa_preference_form, {}).permit(:mfa_preference)) + + if @form.invalid? + flash.now[:errors] = @form.errors.full_messages + return render :preference, status: :unprocessable_entity + end + + if @form.mfa_preference == "software_token" + redirect_to action: :new + return + end + + current_user.update!(mfa_preference: @form.mfa_preference) + redirect_to after_sign_in_path_for(current_user) + end + + # Associate an authenticator app + def new + # We need the access token in order to complete the MFA-setup process, + # so we make sure it's still fresh. If it's not, we get a fresh one by + # forcing the user to log in again + if current_user.access_token_expires_within_minutes?(current_user.access_token, 5) + sign_out(current_user) + redirect_to new_user_session_path + return + end + + @form = Users::AssociateMfaForm.new + @secret_code = auth_service.associate_software_token(current_user.access_token) + @email = current_user.email + end + + def create + @form = Users::AssociateMfaForm.new( + temporary_code: params[:users_associate_mfa_form][:temporary_code] + ) + + if @form.invalid? + return redirect_to({ action: :new }, flash: { errors: @form.errors.full_messages }) + end + + begin + auth_service.verify_software_token(@form.temporary_code, current_user) + rescue Auth::Errors::BaseAuthError => e + return redirect_to({ action: :new }, flash: { errors: [ e.message ] }) + end + + redirect_to root_path, { notice: I18n.t("users.mfa.create.success") } + end + + def destroy + auth_service.disable_software_token(current_user) + redirect_to users_account_path, notice: I18n.t("users.accounts.edit.mfa_successfully_disabled") + end + + private + + def auth_service + AuthService.new + end +end diff --git a/app-rails/app/controllers/users/passwords_controller.rb b/app-rails/app/controllers/users/passwords_controller.rb new file mode 100644 index 0000000..3f262b6 --- /dev/null +++ b/app-rails/app/controllers/users/passwords_controller.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +class Users::PasswordsController < ApplicationController + skip_after_action :verify_authorized + + def forgot + @form = Users::ForgotPasswordForm.new + end + + def send_reset_password_instructions + email = params[:users_forgot_password_form][:email] + @form = Users::ForgotPasswordForm.new(email: email) + + if @form.invalid? + flash.now[:errors] = @form.errors.full_messages + return render :forgot, status: :unprocessable_entity + end + + begin + auth_service.forgot_password(email) + rescue Auth::Errors::BaseAuthError => e + flash.now[:errors] = [ e.message ] + return render :forgot, status: :unprocessable_entity + end + + redirect_to users_reset_password_path + end + + def reset + @form = Users::ResetPasswordForm.new + end + + def confirm_reset + @form = Users::ResetPasswordForm.new(reset_password_params) + + if @form.invalid? + flash.now[:errors] = @form.errors.full_messages + return render :reset, status: :unprocessable_entity + end + + begin + auth_service.confirm_forgot_password( + @form.email, + @form.code, + @form.password + ) + rescue Auth::Errors::BaseAuthError => e + flash.now[:errors] = [ e.message ] + return render :reset, status: :unprocessable_entity + end + + redirect_to new_user_session_path, notice: I18n.t("users.passwords.reset.success") + end + + private + + def auth_service + AuthService.new + end + + def reset_password_params + params.require(:users_reset_password_form).permit(:email, :code, :password) + end +end diff --git a/app-rails/app/controllers/users/registrations_controller.rb b/app-rails/app/controllers/users/registrations_controller.rb new file mode 100644 index 0000000..63fa7f1 --- /dev/null +++ b/app-rails/app/controllers/users/registrations_controller.rb @@ -0,0 +1,86 @@ +# freeze_string_literal: true + +class Users::RegistrationsController < ApplicationController + layout "users" + skip_after_action :verify_authorized + + def new_claimant + @form = Users::RegistrationForm.new(role: "claimant") + render :new + end + + def new_employer + @form = Users::RegistrationForm.new(role: "employer") + render :new + end + + def create + @form = Users::RegistrationForm.new(registration_params) + + if @form.invalid? + flash.now[:errors] = @form.errors.full_messages + return render :new, status: :unprocessable_entity + end + + begin + auth_service.register(@form.email, @form.password, @form.role) + rescue Auth::Errors::BaseAuthError => e + flash.now[:errors] = [ e.message ] + return render :new, status: :unprocessable_entity + end + + redirect_to users_verify_account_path + end + + def new_account_verification + @form = Users::VerifyAccountForm.new() + @resend_verification_form = Users::ResendVerificationForm.new(email: @form.email) + end + + def create_account_verification + @form = Users::VerifyAccountForm.new(verify_account_params) + @resend_verification_form = Users::ResendVerificationForm.new(email: @form.email) + + if @form.invalid? + flash.now[:errors] = @form.errors.full_messages + return render :new_account_verification, status: :unprocessable_entity + end + + begin + auth_service.verify_account(@form.email, @form.code) + rescue Auth::Errors::BaseAuthError => e + flash.now[:errors] = [ e.message ] + return render :new_account_verification, status: :unprocessable_entity + end + + redirect_to new_user_session_path + end + + def resend_verification_code + email = params[:users_resend_verification_form][:email] + @resend_verification_form = Users::ResendVerificationForm.new(email: email) + + if @resend_verification_form.invalid? + flash.now[:errors] = @resend_verification_form.errors.full_messages + return render :new_account_verification, status: :unprocessable_entity + end + + auth_service.resend_verification_code(email) + + flash[:notice] = I18n.t("users.registrations.new_account_verification.resend_success") + redirect_to users_verify_account_path + end + + private + def auth_service + AuthService.new + end + + def registration_params + params.require(:users_registration_form).permit(:email, :password, :password_confirmation, :role) + end + + def verify_account_params + params.require(:users_verify_account_form).permit(:email, :code) + end +end diff --git a/app-rails/app/controllers/users/sessions_controller.rb b/app-rails/app/controllers/users/sessions_controller.rb new file mode 100644 index 0000000..556f146 --- /dev/null +++ b/app-rails/app/controllers/users/sessions_controller.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +class Users::SessionsController < Devise::SessionsController + layout "users" + skip_after_action :verify_authorized + + def new + @form = Users::NewSessionForm.new + end + + def create + @form = Users::NewSessionForm.new(new_session_params) + + if @form.invalid? + flash.now[:errors] = @form.errors.full_messages + return render :new, status: :unprocessable_entity + end + + begin + response = auth_service.initiate_auth( + @form.email, + @form.password + ) + rescue Auth::Errors::UserNotConfirmed => e + return redirect_to users_verify_account_path + rescue Auth::Errors::BaseAuthError => e + flash.now[:errors] = [ e.message ] + return render :new, status: :unprocessable_entity + end + + unless response[:user].present? + puts response.inspect + session[:challenge_session] = response[:session] + session[:challenge_email] = @form.email + return redirect_to session_challenge_path + end + + auth_user(response[:user], response[:access_token]) + end + + # Show MFA + def challenge + if session[:challenge_session].nil? + return redirect_to new_user_session_path + end + + @form = Users::AuthAppCodeForm.new + end + + # Submit MFA + def respond_to_challenge + @form = Users::AuthAppCodeForm.new(params.require(:users_auth_app_code_form).permit(:code)) + + if @form.invalid? + flash.now[:errors] = @form.errors.full_messages + return render :challenge, status: :unprocessable_entity + end + + begin + response = auth_service.respond_to_auth_challenge( + @form.code, { + session: session[:challenge_session], + email: session[:challenge_email] + } + ) + rescue Auth::Errors::BaseAuthError => e + flash.now[:errors] = [ e.message ] + return render :challenge, status: :unprocessable_entity + end + + unless response[:user].present? + flash.now[:errors] = [ "Invalid code" ] + return render :challenge, status: :unprocessable_entity + end + + session[:challenge_session] = nil + session[:challenge_email] = nil + + auth_user(response[:user], response[:access_token]) + end + + # Optionally, you can override the default sign out before: + # DELETE /resource/sign_out + # def destroy + # super + # end + + private + + def auth_service + AuthService.new + end + + def new_session_params + # If :users_new_session_form is renamed, make sure to also update it in + # cognito_authenticatable.rb otherwise login will not work. + params.require(:users_new_session_form).permit(:email, :password) + end + + # This is similar to the default Devise SessionController implementation + # but bypasses warden.authenticate! since we are doing that through Cognito + # https://www.rubydoc.info/github/plataformatec/devise/Devise/SessionsController + def auth_user(user, access_token) + user.access_token = access_token + sign_in(user) + respond_with user, location: after_sign_in_path_for(user) + end +end diff --git a/app-rails/app/javascript/controllers/registrations_controller.js b/app-rails/app/javascript/controllers/registrations_controller.js new file mode 100644 index 0000000..fc97f1e --- /dev/null +++ b/app-rails/app/javascript/controllers/registrations_controller.js @@ -0,0 +1,13 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["resendEmailField"]; + + /** + * Update the hidden field value with the email address entered by the user + * so we have an email for the "resend" form to send to. + */ + updateResendEmail(event) { + this.resendEmailFieldTarget.value = event.currentTarget.value; + } +} diff --git a/app-rails/app/mailers/application_mailer.rb b/app-rails/app/mailers/application_mailer.rb index 3c34c81..36fead4 100644 --- a/app-rails/app/mailers/application_mailer.rb +++ b/app-rails/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" + default from: ENV["SES_EMAIL"] layout "mailer" end diff --git a/app-rails/app/models/concerns/devise/models/cognito_authenticatable.rb b/app-rails/app/models/concerns/devise/models/cognito_authenticatable.rb new file mode 100644 index 0000000..5e56f90 --- /dev/null +++ b/app-rails/app/models/concerns/devise/models/cognito_authenticatable.rb @@ -0,0 +1,27 @@ +# This adds a :cognito_authenticatable accessor for use in the `devise` portion of a user model +# Heavily inspired by https://www.endpointdev.com/blog/2023/01/using-devise-for-authentication-without-database-stored-accounts/ +module Devise + module Models + module CognitoAuthenticatable + extend ActiveSupport::Concern + + module ClassMethods + # Recreates a resource from session data. + # + # It takes as many params as elements in the array returned in + # serialize_into_session. + def serialize_from_session(id, access_token = "") + resource = find(id) + resource.access_token = access_token + resource + end + + # Returns an array with the data from the user that needs to be + # serialized into the session. + def serialize_into_session(user) + [ user.id, user.access_token ] + end + end + end + end +end diff --git a/app-rails/app/models/user.rb b/app-rails/app/models/user.rb new file mode 100644 index 0000000..7737520 --- /dev/null +++ b/app-rails/app/models/user.rb @@ -0,0 +1,40 @@ +class User < ApplicationRecord + devise :cognito_authenticatable, :timeoutable + attr_accessor :access_token + + # == Enums ======================================================== + # See `technical-foundation.md#enums` for important note about enums. + enum :mfa_preference, { opt_out: 0, software_token: 1 }, validate: { allow_nil: true } + + # == Relationships ======================================================== + has_many :tasks + has_one :user_role, dependent: :destroy + + # == Validations ========================================================== + validates :provider, presence: true + + # == Methods ============================================================== + def claimant? + user_role&.claimant? + end + + def employer? + user_role&.employer? + end + + def superadmin? + # @TODO: Obviously replace this with a real implementation once admin authentication is implemented + email.ends_with?("+admin@navapbc.com") + end + + # Check if the access token is expired or will expire within the next `minutes` minutes. + # Access token is only stored in the session, so it needs passed in, rather than accessed from the model. + def access_token_expires_within_minutes?(access_token, minutes) + return true unless access_token.present? + + decoded_token = JWT.decode(access_token, nil, false) + expiration_time = Time.at(decoded_token.first["exp"]) + + expiration_time < Time.now + minutes.minutes + end +end diff --git a/app-rails/app/models/user_role.rb b/app-rails/app/models/user_role.rb new file mode 100644 index 0000000..f4b2c7b --- /dev/null +++ b/app-rails/app/models/user_role.rb @@ -0,0 +1,8 @@ +class UserRole < ApplicationRecord + # == Enums ================================================================ + # See `technical-foundation.md#enums` for important note about enums. + enum :role, { claimant: 0, employer: 1 }, default: :claimant, validate: true + + # == Relationships ======================================================== + belongs_to :user +end diff --git a/app-rails/app/views/layouts/employers.html.erb b/app-rails/app/views/layouts/employers.html.erb new file mode 100644 index 0000000..62197c5 --- /dev/null +++ b/app-rails/app/views/layouts/employers.html.erb @@ -0,0 +1,23 @@ +<%= content_for :content_col_class, 'tablet:grid-col-8 desktop:grid-col-9' %> +<% nav_items = [ + { + path: if @employer then employer_path(@employer) else employers_path end, + label: t('.sidebar.index') + }, + { path: employer_private_plan_requests_path(@employer), label: t('.sidebar.private_plans') }, + { path: employer_seasonal_employment_requests_path(@employer), label: t('.sidebar.seasonal_employees') }, +] %> + +<%= content_for :before_content_col do %> + +<% end %> + +<%= render template: 'layouts/application' %> \ No newline at end of file diff --git a/app-rails/app/views/layouts/users.html.erb b/app-rails/app/views/layouts/users.html.erb new file mode 100644 index 0000000..a483ed8 --- /dev/null +++ b/app-rails/app/views/layouts/users.html.erb @@ -0,0 +1,10 @@ +<%= content_for :main_col_class, 'bg-base-lightest' %> +<%= content_for :content_col_class, 'tablet:grid-col-6 flex-align-self-center' %> + +<%= content_for :after_content_col do %> +
+ <%= yield :sidebar %> +
+<% end %> + +<%= render template: 'layouts/application' %> \ No newline at end of file diff --git a/app-rails/bin/rspec b/app-rails/bin/rspec new file mode 100755 index 0000000..cc82ce7 --- /dev/null +++ b/app-rails/bin/rspec @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +# Since CI runs tests from within the container, which +# sets RAILS_ENV to development, we need to override it: +export RAILS_ENV=test + +bundle exec rspec --format documentation diff --git a/app-rails/config/application.rb b/app-rails/config/application.rb index 5448663..a92e338 100644 --- a/app-rails/config/application.rb +++ b/app-rails/config/application.rb @@ -6,15 +6,30 @@ # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) -module TemplateApplicationRails +module Pfml class Application < Rails::Application + # Internationalization + I18n.available_locales = [ :"en", :"es-US" ] + I18n.default_locale = :"en" + I18n.enforce_available_locales = true + + # Support nested locale files + config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}")] + # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.1 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. # Common ones are `templates`, `generators`, or `middleware`, for example. - config.autoload_lib(ignore: %w(assets tasks)) + config.autoload_lib(ignore: %w[assets tasks generators]) + + # Prevent the form_with helper from wrapping input and labels with separate + # div elements when an error is present, since this breaks USWDS styling + # and functionality. + config.action_view.field_error_proc = Proc.new { |html_tag, instance| + html_tag + } # Configuration for the application, engines, and railties goes here. # @@ -23,5 +38,16 @@ class Application < Rails::Application # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") + + config.generators do |g| + g.factory_bot suffix: "factory" + end + + # Support UUID generation. This was a callout in the ActiveStorage guide + # https://edgeguides.rubyonrails.org/active_storage_overview.html#setup + Rails.application.config.generators { |g| g.orm :active_record, primary_key_type: :uuid } + + # Show a 403 Forbidden error page when Pundit raises a NotAuthorizedError + config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden end end diff --git a/app-rails/config/database.yml b/app-rails/config/database.yml index b2e35d7..efbee17 100644 --- a/app-rails/config/database.yml +++ b/app-rails/config/database.yml @@ -19,9 +19,16 @@ default: &default # https://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + # Support environment variables. Set defaults. + database: <%= ENV.fetch("DB_NAME") { nil } %> + username: <%= ENV.fetch("DB_USER") { nil } %> + password: <%= ENV.fetch("DB_PASSWORD") { nil } %> + host: <%= ENV.fetch("DB_HOST") { "localhost" } %> + port: <%= ENV.fetch("DB_PORT") { 5432 } %> + schema_search_path: <%= ENV.fetch("DB_SCHEMA") { public } %> + development: <<: *default - database: template_application_rails_development # The specified database role being used to connect to PostgreSQL. # To create additional roles in PostgreSQL see `$ createuser --help`. @@ -79,6 +86,6 @@ test: # production: <<: *default - database: template_application_rails_production - username: template_application_rails - password: <%= ENV["TEMPLATE_APPLICATION_RAILS_DATABASE_PASSWORD"] %> + # Enable AWS RDS IAM Auth token generator + # For more details, see: https://github.com/haines/pg-aws_rds_iam + aws_rds_iam_auth_token_generator: default diff --git a/app-rails/config/environments/development.rb b/app-rails/config/environments/development.rb index f2aa3d1..41bc7bd 100644 --- a/app-rails/config/environments/development.rb +++ b/app-rails/config/environments/development.rb @@ -1,7 +1,7 @@ require "active_support/core_ext/integer/time" # Custom setting: set the default url. -Rails.application.default_url_options = { host: ENV["APP_HOST"], port: ENV["APP_PORT"]} +Rails.application.default_url_options = { host: ENV["APP_HOST"], port: ENV["APP_PORT"] } Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -36,11 +36,9 @@ config.cache_store = :null_store end - # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local + config.active_storage.service = if ENV["BUCKET_NAME"] then :amazon else :local end - # Custom setting: Use letter opener gem for local mail delivery. - config.action_mailer.delivery_method = :letter_opener + config.action_mailer.delivery_method = if ENV["SES_EMAIL"] then :sesv2 else :letter_opener end # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false @@ -78,5 +76,5 @@ # config.action_cable.disable_request_forgery_protection = true # Raise error when a before_action's only/except options reference missing actions - config.action_controller.raise_on_missing_callback_actions = true + # config.action_controller.raise_on_missing_callback_actions = true end diff --git a/app-rails/config/environments/production.rb b/app-rails/config/environments/production.rb index 7acdab9..de6ef19 100644 --- a/app-rails/config/environments/production.rb +++ b/app-rails/config/environments/production.rb @@ -1,7 +1,7 @@ require "active_support/core_ext/integer/time" # Custom setting: set the default url. -Rails.application.default_url_options = { host: ENV["APP_HOST"], port: ENV["APP_PORT"]} +Rails.application.default_url_options = { host: ENV["APP_HOST"], port: ENV["APP_PORT"] } Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -40,7 +40,7 @@ # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local + config.active_storage.service = :amazon # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil @@ -74,6 +74,7 @@ # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "template_application_rails_production" + config.action_mailer.delivery_method = :sesv2 config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. diff --git a/app-rails/config/environments/test.rb b/app-rails/config/environments/test.rb index a57a0f1..785ed87 100644 --- a/app-rails/config/environments/test.rb +++ b/app-rails/config/environments/test.rb @@ -63,5 +63,5 @@ # config.action_view.annotate_rendered_view_with_filenames = true # Raise error when a before_action's only/except options reference missing actions - config.action_controller.raise_on_missing_callback_actions = true + # config.action_controller.raise_on_missing_callback_actions = true end diff --git a/app-rails/config/initializers/devise.rb b/app-rails/config/initializers/devise.rb new file mode 100644 index 0000000..b0dbf4d --- /dev/null +++ b/app-rails/config/initializers/devise.rb @@ -0,0 +1,301 @@ +# frozen_string_literal: true + +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # ==> Support AWS Cognito login + Devise.add_module :cognito_authenticatable, controller: :sessions, route: :session + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + config.timeout_in = 15.minutes + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html, :turbo_stream] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> Hotwire/Turbo configuration + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found` respectively, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require "devise/orm/active_record" + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [ :http_auth ] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '95130f242f8b18bee7a505a7b6819dcdc2f274771201c39b2627425f38e5e81e060c48c839815f975aeabe26d55de0b00243fd1b5b4170c3a117102e57308660' + + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + # config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + # config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + # config.strip_whitespace_keys = [ :email ] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 12. If + # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + # config.stretches = Rails.env.test? ? 1 : 12 + + # Set up a pepper to generate the hashed password. + # config.pepper = '3726e219e02fbaed55cfcd17bdec435e8af363d24585278cde8f03f826f9e1a56f6fdd7bb487e440ac057d78d1d9aeff96756d8daf8b962b52c0e1b5bad25f27' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + # config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :validatable + # Range for password length. + # config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + # config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + # config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true +end diff --git a/app-rails/config/locales/defaults/en.yml b/app-rails/config/locales/defaults/en.yml new file mode 100644 index 0000000..8dad02d --- /dev/null +++ b/app-rails/config/locales/defaults/en.yml @@ -0,0 +1,59 @@ +en: + activerecord: + errors: + messages: + # Override error messages: https://guides.rubyonrails.org/i18n.html#error-message-interpolation + # blank: "'%{attribute}' can't be blank" + # + # i18n strings for ActiveStorageValidation + # See: https://github.com/igorkasyanchuk/active_storage_validations + content_type_invalid: "has an invalid content type" + file_size_not_less_than: "file size must be less than %{max_size} (current size is %{file_size})" + file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max_size} (current size is %{file_size})" + file_size_not_greater_than: "file size must be greater than %{min_size} (current size is %{file_size})" + file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min_size} (current size is %{file_size})" + file_size_not_between: "file size must be between %{min_size} and %{max_size} (current size is %{file_size})" + limit_out_of_range: "total number is out of range" + image_metadata_missing: "is not a valid image" + dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel." + dimension_max_inclusion: "must be less than or equal to %{width} x %{height} pixel." + dimension_width_inclusion: "width is not included between %{min} and %{max} pixel." + dimension_height_inclusion: "height is not included between %{min} and %{max} pixel." + dimension_width_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel." + dimension_height_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel." + dimension_width_less_than_or_equal_to: "width must be less than or equal to %{length} pixel." + dimension_height_less_than_or_equal_to: "height must be less than or equal to %{length} pixel." + dimension_width_equal_to: "width must be equal to %{length} pixel." + dimension_height_equal_to: "height must be equal to %{length} pixel." + aspect_ratio_not_square: "must be a square image" + aspect_ratio_not_portrait: "must be a portrait image" + aspect_ratio_not_landscape: "must be a landscape image" + aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}" + aspect_ratio_unknown: "has an unknown aspect ratio" + image_not_processable: "is not a valid image" + auth: + errors: + messages: + code_mismatch: "The code you entered is incorrect" + code_expired: "The code you entered has expired" + invalid_credentials: "Invalid email or password" + invalid_software_token: "Invalid code. Please try again." + invalid_password_format: "Password must be at least 12 characters long" + username_exists: "An account with the given email already exists" + user_not_confirmed: "You must confirm your email address first" + date: + formats: + default: "%m/%d/%Y" + short: "%b %d" + long: "%B %d, %Y" + helpers: + submit: + create: "Submit" + update: "Update" + time: + formats: + default: "%B %d, %Y" + long: "%B %d, %Y %-I:%M %p" + us_form_with: + boolean_true: "Yes" + boolean_false: "No" diff --git a/app-rails/config/locales/defaults/es-US.yml b/app-rails/config/locales/defaults/es-US.yml new file mode 100644 index 0000000..3146c4e --- /dev/null +++ b/app-rails/config/locales/defaults/es-US.yml @@ -0,0 +1,17 @@ +es-US: + date: + formats: + default: "%m/%d/%Y" + short: "%b %d" + long: "%B %d, %Y" + helpers: + submit: + create: "Submit" + update: "Update" + time: + formats: + default: "%B %d, %Y" + long: "%B %d, %Y %-I:%M %p" + us_form_with: + boolean_true: "Sí" + boolean_false: "No" diff --git a/app-rails/config/locales/devise.en.yml b/app-rails/config/locales/devise.en.yml new file mode 100644 index 0000000..260e1c4 --- /dev/null +++ b/app-rails/config/locales/devise.en.yml @@ -0,0 +1,65 @@ +# Additional translations at https://github.com/heartcombo/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + email_changed: + subject: "Email Changed" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." + updated: "Your account has been updated successfully." + updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/app-rails/config/locales/en.yml b/app-rails/config/locales/en.yml index 6c349ae..80db087 100644 --- a/app-rails/config/locales/en.yml +++ b/app-rails/config/locales/en.yml @@ -1,31 +1,41 @@ -# Files in the config/locales directory are used for internationalization and -# are automatically loaded by Rails. If you want to use locales other than -# English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t "hello" -# -# In views, this is aliased to just `t`: -# -# <%= t("hello") %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# To learn more about the API, please read the Rails Internationalization guide -# at https://guides.rubyonrails.org/i18n.html. +# See: https://guides.rubyonrails.org/i18n.html. # # Be aware that YAML interprets the following case-insensitive strings as # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings # must be quoted to be interpreted as strings. For example: -# -# en: -# "yes": yup -# enabled: "ON" +# "yes": yup +# enabled: "ON" en: - hello: "Hello world" + header: + account: My account + language_toggle: English + logout: "Log out" + title: "Paid Family & Medical Leave" + close: "Close" + flash: + error_heading: + zero: "An unexpected error occurred" + one: "An error occurred" + other: "%{count} errors occurred" + error_fallback: "Sorry, we encountered an unexpected error. If this continues to happen, you may call the Paid Family Leave Contact Center." + reload_page: "Reload page" + breadcrumbs: + home: "Home" + buttons: + submit: "Submit" + requestable_step_indicator: + submitted: "Submitted" + in_review: "In review" + determination_made: "Determination made" + requestable_index: + existing_requests: + heading: "Existing requests" + col_created_at: "Date submitted" + col_status: "Status" + col_actions: "Actions" + action_view_rfi: "Respond to RFI" + action_none: "You have no pending actions at this time." + tables: + column: "Column" + description: "Description" diff --git a/app-rails/config/locales/models/user/en.yml b/app-rails/config/locales/models/user/en.yml new file mode 100644 index 0000000..8837148 --- /dev/null +++ b/app-rails/config/locales/models/user/en.yml @@ -0,0 +1,5 @@ +en: + activemodel: + attributes: + "users/mfa_preference_form": + mfa_preference: "Multi-factor authentication preference" diff --git a/app-rails/config/locales/views/application/en.yml b/app-rails/config/locales/views/application/en.yml new file mode 100644 index 0000000..80db087 --- /dev/null +++ b/app-rails/config/locales/views/application/en.yml @@ -0,0 +1,41 @@ +# See: https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# "yes": yup +# enabled: "ON" + +en: + header: + account: My account + language_toggle: English + logout: "Log out" + title: "Paid Family & Medical Leave" + close: "Close" + flash: + error_heading: + zero: "An unexpected error occurred" + one: "An error occurred" + other: "%{count} errors occurred" + error_fallback: "Sorry, we encountered an unexpected error. If this continues to happen, you may call the Paid Family Leave Contact Center." + reload_page: "Reload page" + breadcrumbs: + home: "Home" + buttons: + submit: "Submit" + requestable_step_indicator: + submitted: "Submitted" + in_review: "In review" + determination_made: "Determination made" + requestable_index: + existing_requests: + heading: "Existing requests" + col_created_at: "Date submitted" + col_status: "Status" + col_actions: "Actions" + action_view_rfi: "Respond to RFI" + action_none: "You have no pending actions at this time." + tables: + column: "Column" + description: "Description" diff --git a/app-rails/config/locales/views/application/es-US.yml b/app-rails/config/locales/views/application/es-US.yml new file mode 100644 index 0000000..c988031 --- /dev/null +++ b/app-rails/config/locales/views/application/es-US.yml @@ -0,0 +1,5 @@ +es-US: + header: + language_toggle: EspaÃąol + logout: Cerrar sesiÃģn + title: Permiso Familiar y MÃĐdico Pagado diff --git a/app-rails/config/locales/views/home/en.yml b/app-rails/config/locales/views/home/en.yml new file mode 100644 index 0000000..31f54b7 --- /dev/null +++ b/app-rails/config/locales/views/home/en.yml @@ -0,0 +1,12 @@ +en: + home: + index: + title: "Get started" + intro: "You can either apply for leave or manage claims for an organization. If you need to do both, you will need to create separate accounts." + claimant_heading: "Claimants" + claimant_body: "I am applying for paid leave benefits." + claimant_signup: "Create a Claimant account" + employer_heading: "Employers" + employer_body: "I manage claims for the employees in my organization." + employer_signup: "Create an Employer account" + or_sign_in: "Or sign into an existing account" diff --git a/app-rails/config/locales/views/users/en.yml b/app-rails/config/locales/views/users/en.yml new file mode 100644 index 0000000..9787d0a --- /dev/null +++ b/app-rails/config/locales/views/users/en.yml @@ -0,0 +1,77 @@ +en: + users: + hide_password: "Hide password" + show_password: "Show password" + password_hint: "Your password must be 12 characters or longer. Don’t use common phrases or repeated characters, like abc or 111." + accounts: + edit: + title: "My account" + change_email: "Change email" + change_password: "Change password" + change_password_instructions: "A 6-digit code will be emailed to you to start the password reset process." + mfa_preference: "Multi-factor authentication" + disable_mfa: "Disable multi-factor authentication" + enable_mfa: "Enable multi-factor authentication" + mfa_successfully_disabled: "Multi-factor authentication has been disabled." + passwords: + forgot: + title: "Forgot your password?" + instructions: "If an account exists for the email you provide, we will email a 6-digit verification code to it." + submit: "Send code" + reset: + title: "Create a new password" + instructions_heading: "Check your email" + instructions_html: | + We sent a 6-digit code to your email. If you don’t see that email, check your spam folder. + If you didn’t get an email with a code, you may not have an account with that email, or you may need to verify your email address first. + code: "6-digit code" + submit: "Set password" + success: "Your password has been reset." + sessions: + new: + title: "Sign in" + forgot_password: "Forgot your password?" + submit: "Log in" + no_account: "Don't have an account?" + create_account: "Create your account now" + challenge: + title: "Enter your authentication app code" + code: "6-digit code" + submit: "Submit" + registrations: + new: + are_claimant_heading: "Are you applying for leave?" + are_claimant_body: "To apply for leave, create a claimant account." + are_claimant_action: "Create a Claimant account" + are_employer_heading: "Are you an employer?" + are_employer_body: "To manage claims for an organization, create an employer account." + are_employer_action: "Create an Employer account" + have_existing_account: "Already have an account?" + login: "Log in" + title_claimant: "Create a Claimant account" + title_employer: "Create an Employer account" + new_account_verification: + title: "Verify your email address" + instructions: "We sent a 6 digit verification code to your email. Enter the code to verify your email." + code: "6-digit code" + resend: "Send a new code" + resend_success: "If you have an account under that email, then a new code has been sent." + mfa: + new: + title: "Add an authentication app" + lead: "Set up an authentication app to sign in using temporary security codes." + auth_apps_heading: "What's an authentication app?" + auth_apps_html: "Authentication applications are downloaded to your device and generate secure, six-digit codes you use to sign in to your accounts. Some popular options include: Google Authenticator, Authy, LastPass, 1Password." + qr_heading: "Scan this QR barcode with your app" + code_label: "Or enter this code manually into your authentication app" + temporary_code: "Enter the temporary code from your app" + open_app: "Open your authentication app" + create: + success: "Successfully added an authentication app" + preference: + title: Would you like to set up multi-factor authentication? + preference: Add an additional layer of protection to your account by selecting a multi-factor authentication method. + software_token_label: Authentication application + software_token_hint: "Download or use an authentication app of your choice to generate secure codes." + opt_out_label: "Skip for now" + opt_out_hint: "You can set up multi-factor authentication later in your account settings." diff --git a/app-rails/config/routes.rb b/app-rails/config/routes.rb index a125ef0..614a461 100644 --- a/app-rails/config/routes.rb +++ b/app-rails/config/routes.rb @@ -3,8 +3,50 @@ # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. - get "up" => "rails/health#show", as: :rails_health_check + get "health" => "rails/health#show", as: :rails_health_check + # Keep the default /up rails endpoint. + get "up" => "rails/health#show" - # Defines the root path route ("/") - # root "posts#index" + # Support locale prefixes for these routes: + localized do + # Defines the root path route ("/") + root "home#index" + + # Session management + devise_for :users, controllers: { sessions: "users/sessions" } + devise_scope :user do + get "sessions/challenge" => "users/sessions#challenge", as: :session_challenge + post "sessions/challenge" => "users/sessions#respond_to_challenge", as: :respond_to_session_challenge + end + + # Registration and account management + namespace :users do + resources :registrations, only: [ :create ] + get "registrations/claimant" => "registrations#new_claimant", as: :new_claimant_registration + get "registrations/employer" => "registrations#new_employer", as: :new_employer_registration + + resources :mfa, only: [ :new, :create ] + get "mfa/preference" => "mfa#preference", as: :mfa_preference + post "mfa/preference" => "mfa#update_preference", as: :update_mfa_preference + delete "mfa" => "mfa#destroy", as: :disable_mfa + + get "forgot-password" => "passwords#forgot" + post "forgot-password" => "passwords#send_reset_password_instructions" + get "reset-password" => "passwords#reset" + post "reset-password" => "passwords#confirm_reset" + + get "verify-account" => "registrations#new_account_verification" + post "verify-account" => "registrations#create_account_verification" + post "resend-verification" => "registrations#resend_verification_code" + + get "account" => "accounts#edit" + patch "account/email" => "accounts#update_email" + end + + # Development-only sandbox + namespace :dev do + get "sandbox" + post "send_email" + end + end end diff --git a/pfml/app/adapters/auth/cognito_adapter.rb b/pfml/app/adapters/auth/cognito_adapter.rb new file mode 100644 index 0000000..9ddad83 --- /dev/null +++ b/pfml/app/adapters/auth/cognito_adapter.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true + +require "aws-sdk-cognitoidentityprovider" + +class Auth::CognitoAdapter + @@provider_name = "cognito" + + def initialize(client: Aws::CognitoIdentityProvider::Client.new) + @client = client + end + + # Add a user to the Cognito user pool + # https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminCreateUser.html + def create_account(email, password) + begin + response = @client.sign_up( + client_id: ENV["COGNITO_CLIENT_ID"], + secret_hash: get_secret_hash(email), + username: email, + password: password, + user_attributes: [ + { + name: "email", + value: email + } + ] + ) + rescue Aws::CognitoIdentityProvider::Errors::UsernameExistsException, + Aws::CognitoIdentityProvider::Errors::InvalidPasswordException => e + raise to_auth_error(e) + rescue Aws::CognitoIdentityProvider::Errors::InvalidParameterException => e + # For some reason, Cognito raises InvalidParameterException when the password + # is a single character, instead of InvalidPasswordException. + raise Auth::Errors::InvalidPasswordFormat + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + + { + uid: response.user_sub, + confirmation_channel: response.code_delivery_details.delivery_medium, + provider: @@provider_name + } + end + + # https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ForgotPassword.html + def forgot_password(email) + begin + response = @client.forgot_password( + client_id: ENV["COGNITO_CLIENT_ID"], + secret_hash: get_secret_hash(email), + username: email + ) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + + { + confirmation_channel: response.code_delivery_details.delivery_medium + } + end + + # https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ConfirmForgotPassword.html + def confirm_forgot_password(email, code, password) + begin + @client.confirm_forgot_password( + client_id: ENV["COGNITO_CLIENT_ID"], + secret_hash: get_secret_hash(email), + username: email, + confirmation_code: code, + password: password + ) + rescue Aws::CognitoIdentityProvider::Errors::CodeMismatchException, + Aws::CognitoIdentityProvider::Errors::ExpiredCodeException, + Aws::CognitoIdentityProvider::Errors::InvalidPasswordException, + Aws::CognitoIdentityProvider::Errors::UserNotConfirmedException => e + raise to_auth_error(e) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + end + + # https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminUpdateUserAttributes.html + def change_email(uid, new_email) + begin + response = @client.admin_update_user_attributes( + user_pool_id: ENV["COGNITO_USER_POOL_ID"], + username: uid, + user_attributes: [ + { + name: "email", + value: new_email + }, + # Since we don't store the user's access token after they authenticate, + # we can't use update_user_attributes, which includes an email verification + # step (via verify_user_attribute). For now, we don't include a verification step + # when changing email. If we want that, we need to find a way to store the user's + # access token and keep it fresh. + { + name: "email_verified", + value: "true" + } + ] + ) + rescue Aws::CognitoIdentityProvider::Errors::AliasExistsException, + Aws::CognitoIdentityProvider::Errors::UsernameExistsException => e + raise to_auth_error(e) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + end + + # https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_InitiateAuth.html + def initiate_auth(email, password) + begin + response = @client.admin_initiate_auth( + user_pool_id: ENV["COGNITO_USER_POOL_ID"], + client_id: ENV["COGNITO_CLIENT_ID"], + auth_flow: "ADMIN_USER_PASSWORD_AUTH", + auth_parameters: { + "USERNAME" => email, + "PASSWORD" => password, + "SECRET_HASH" => get_secret_hash(email) + } + ) + rescue Aws::CognitoIdentityProvider::Errors::NotAuthorizedException, + Aws::CognitoIdentityProvider::Errors::UserNotConfirmedException => e + raise to_auth_error(e) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + + if response.challenge_name.nil? + return get_auth_result(response) + end + + { + challenge_name: response.challenge_name, + # If you must pass another challenge, this parameter should be used to compute + # inputs to the next call (AdminRespondToAuthChallenge). + challenge_parameters: response.challenge_parameters, + # The session that should be passed both ways in challenge-response calls to the service. + # If AdminInitiateAuth or AdminRespondToAuthChallenge API call determines that the caller + # must pass another challenge, they return a session with other challenge parameters. This + # session should be passed as it is to the next AdminRespondToAuthChallenge API call. + session: response.session + } + end + + # https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ResendConfirmationCode.html + def resend_verification_code(email) + begin + @client.resend_confirmation_code( + client_id: ENV["COGNITO_CLIENT_ID"], + secret_hash: get_secret_hash(email), + username: email + ) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + end + + def respond_to_auth_challenge(code, challenge = {}) + begin + response = @client.admin_respond_to_auth_challenge( + client_id: ENV["COGNITO_CLIENT_ID"], + user_pool_id: ENV["COGNITO_USER_POOL_ID"], + challenge_name: "SOFTWARE_TOKEN_MFA", + session: challenge[:session], + challenge_responses: { + "SECRET_HASH" => get_secret_hash(challenge[:email]), + "SOFTWARE_TOKEN_MFA_CODE" => code, + "USERNAME" => challenge[:email] + } + ) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + + get_auth_result(response) + end + + # https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ConfirmSignUp.html + def verify_account(email, code) + begin + @client.confirm_sign_up( + client_id: ENV["COGNITO_CLIENT_ID"], + secret_hash: get_secret_hash(email), + username: email, + confirmation_code: code + ) + rescue Aws::CognitoIdentityProvider::Errors::CodeMismatchException, + Aws::CognitoIdentityProvider::Errors::ExpiredCodeException => e + raise to_auth_error(e) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + end + + def associate_software_token(access_token) + begin + response = @client.associate_software_token( + access_token: access_token + ) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + + response.secret_code + end + + # https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_VerifySoftwareToken.html + def verify_software_token(code, access_token) + begin + response = @client.verify_software_token( + access_token: access_token, + user_code: code + ) + rescue Aws::CognitoIdentityProvider::Errors::EnableSoftwareTokenMFAException => e + raise to_auth_error(e) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + + unless response.status == "SUCCESS" + raise Auth::Errors::InvalidSoftwareToken + end + + begin + @client.set_user_mfa_preference( + access_token: access_token, + software_token_mfa_settings: { + enabled: true, + preferred_mfa: true + } + ) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + end + + def disable_software_token(uid) + begin + @client.admin_set_user_mfa_preference( + user_pool_id: ENV["COGNITO_USER_POOL_ID"], + username: uid, + software_token_mfa_settings: { + enabled: false, + preferred_mfa: false + } + ) + rescue Aws::CognitoIdentityProvider::Errors::ServiceError => e + raise Auth::Errors::ProviderError.new(e.message) + end + end + + private + # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CognitoIdentityProvider/Types/AuthenticationResultType.html + def get_auth_result(response) + { + provider: @@provider_name, + uid: get_sub_from_id_token(response.authentication_result.id_token), + access_token: response.authentication_result.access_token + } + end + + def get_secret_hash(username) + message = username + ENV["COGNITO_CLIENT_ID"] + key = ENV["COGNITO_CLIENT_SECRET"] + Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", key, message)) + end + + def get_sub_from_id_token(id_token) + JWT.decode(id_token, nil, false)[0]["sub"] + end + + # Convert an AWS Cognito SDK exception to our app's auth error equivalent + def to_auth_error(error) + error_map = { + "AliasExistsException" => Auth::Errors::UsernameExists, + "CodeMismatchException" => Auth::Errors::CodeMismatch, + "EnableSoftwareTokenMFAException" => Auth::Errors::InvalidSoftwareToken, + "ExpiredCodeException" => Auth::Errors::CodeExpired, + "InvalidPasswordException" => Auth::Errors::InvalidPasswordFormat, + "NotAuthorizedException" => Auth::Errors::InvalidCredentials, + "UsernameExistsException" => Auth::Errors::UsernameExists, + "UserNotConfirmedException" => Auth::Errors::UserNotConfirmed + } + + error_map[error.class.name.demodulize] || error + end +end diff --git a/pfml/app/adapters/auth/errors.rb b/pfml/app/adapters/auth/errors.rb new file mode 100644 index 0000000..a5a4e12 --- /dev/null +++ b/pfml/app/adapters/auth/errors.rb @@ -0,0 +1,51 @@ + module Auth + module Errors + class BaseAuthError < StandardError + def message + error_key = self.class.name.demodulize.underscore + I18n.t("auth.errors.messages.#{error_key}", default: self.class.name.demodulize) + end + end + + # User tried to use an expired verification code + class CodeExpired < BaseAuthError + end + + # 6-digit verification code entered by user does not match the one sent to their email + class CodeMismatch < BaseAuthError + end + + # User tried to login with invalid email or password. + # We intentionally do not distinguish between the two in order to prevent enumeration attacks. + class InvalidCredentials < BaseAuthError + end + + class InvalidPasswordFormat < BaseAuthError + end + + # Fallback error for when we don't know what went wrong, so we just show the error + # message from the provider + class ProviderError < BaseAuthError + def initialize(message) + @message = message + end + + def message + @message + end + end + + # MFA authenticator code entered by user does not match the one generated by their device, + # which means it may have expired or the user entered it incorrectly + class InvalidSoftwareToken < BaseAuthError + end + + # User tried to register with, or change their email to, an email that already exists + class UsernameExists < BaseAuthError + end + + # User tried to do something that requires a verified email, but their email is not verified + class UserNotConfirmed < BaseAuthError + end + end + end diff --git a/pfml/app/adapters/auth/mock_adapter.rb b/pfml/app/adapters/auth/mock_adapter.rb new file mode 100644 index 0000000..aab9dd7 --- /dev/null +++ b/pfml/app/adapters/auth/mock_adapter.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +class Auth::MockAdapter + def initialize(uid_generator: -> { SecureRandom.uuid }) + @uid_generator = uid_generator + end + + def self.provider_name + @@provider_name + end + + def create_account(email, password) + if email.include?("UsernameExists") + raise Auth::Errors::UsernameExists.new + end + + { + uid: @uid_generator.call, + confirmation_channel: "EMAIL", + provider: "mock" + } + end + + def change_email(uid, new_email) + end + + def forgot_password(email) + if email.include?("UsernameExists") + raise Auth::Errors::UsernameExists.new + end + + { + confirmation_channel: "EMAIL" + } + end + + def confirm_forgot_password(email, code, password) + if code == "000001" + raise Auth::Errors::CodeMismatch.new + end + end + + def initiate_auth(email, password) + if email.include?("unconfirmed") + raise Auth::Errors::UserNotConfirmed.new + elsif password == "wrong" + raise Auth::Errors::InvalidCredentials.new + elsif email.include?("mfa") + return { + "challenge_name": "SOFTWARE_TOKEN_MFA", + "session": "mock-session" + } + end + + { + uid: @uid_generator.call, + provider: "mock" + } + end + + def associate_software_token(access_token) + "mock-secret" + end + + def verify_software_token(code, access_token) + if code == "000001" + raise Auth::Errors::InvalidSoftwareToken.new + end + end + + def disable_software_token(uid) + end + + def respond_to_auth_challenge(code, challenge) + if code == "000001" + raise Auth::Errors::InvalidSoftwareToken.new + end + + { + uid: @uid_generator.call, + provider: "mock" + } + end + + def verify_account(email, code) + if code == "000001" + raise Auth::Errors::CodeMismatch.new + end + + {} + end +end diff --git a/pfml/app/forms/users/associate_mfa_form.rb b/pfml/app/forms/users/associate_mfa_form.rb new file mode 100644 index 0000000..024584a --- /dev/null +++ b/pfml/app/forms/users/associate_mfa_form.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Users::AssociateMfaForm + include ActiveModel::Model + + attr_accessor :temporary_code + + validates :temporary_code, length: { is: 6 } +end diff --git a/pfml/app/forms/users/auth_app_code_form.rb b/pfml/app/forms/users/auth_app_code_form.rb new file mode 100644 index 0000000..54a8a83 --- /dev/null +++ b/pfml/app/forms/users/auth_app_code_form.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Users::AuthAppCodeForm + include ActiveModel::Model + + attr_accessor :code + + validates :code, length: { is: 6 } +end diff --git a/pfml/app/forms/users/forgot_password_form.rb b/pfml/app/forms/users/forgot_password_form.rb new file mode 100644 index 0000000..3b26d30 --- /dev/null +++ b/pfml/app/forms/users/forgot_password_form.rb @@ -0,0 +1,7 @@ +class Users::ForgotPasswordForm + include ActiveModel::Model + + attr_accessor :email + + validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } +end diff --git a/pfml/app/forms/users/mfa_preference_form.rb b/pfml/app/forms/users/mfa_preference_form.rb new file mode 100644 index 0000000..0cc0b8e --- /dev/null +++ b/pfml/app/forms/users/mfa_preference_form.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Users::MfaPreferenceForm + include ActiveModel::Model + + attr_accessor :mfa_preference + + validates :mfa_preference, presence: true + validates :mfa_preference, inclusion: { in: User.mfa_preferences.keys }, if: -> { mfa_preference.present? } +end diff --git a/pfml/app/forms/users/new_session_form.rb b/pfml/app/forms/users/new_session_form.rb new file mode 100644 index 0000000..6135b62 --- /dev/null +++ b/pfml/app/forms/users/new_session_form.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Users::NewSessionForm + include ActiveModel::Model + + attr_accessor :email, :password + + validates :email, :password, presence: true + validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, if: -> { email.present? } +end diff --git a/pfml/app/forms/users/registration_form.rb b/pfml/app/forms/users/registration_form.rb new file mode 100644 index 0000000..250ac6d --- /dev/null +++ b/pfml/app/forms/users/registration_form.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class Users::RegistrationForm + include ActiveModel::Model + + attr_accessor :email, :password, :password_confirmation, :role + + validates :email, :password, :role, presence: true + validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, if: -> { email.present? } + + validates :password, confirmation: true, if: -> { password.present? } +end diff --git a/pfml/app/forms/users/resend_verification_form.rb b/pfml/app/forms/users/resend_verification_form.rb new file mode 100644 index 0000000..16ecfb2 --- /dev/null +++ b/pfml/app/forms/users/resend_verification_form.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Users::ResendVerificationForm + include ActiveModel::Model + + attr_accessor :email + + validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } +end diff --git a/pfml/app/forms/users/reset_password_form.rb b/pfml/app/forms/users/reset_password_form.rb new file mode 100644 index 0000000..5fdab55 --- /dev/null +++ b/pfml/app/forms/users/reset_password_form.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Users::ResetPasswordForm + include ActiveModel::Model + + attr_accessor :email, :password, :code + + validates :email, :password, :code, presence: true + validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, if: -> { email.present? } + validates :code, length: { is: 6 }, if: -> { code.present? } +end diff --git a/pfml/app/forms/users/update_email_form.rb b/pfml/app/forms/users/update_email_form.rb new file mode 100644 index 0000000..8df0f7d --- /dev/null +++ b/pfml/app/forms/users/update_email_form.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Users::UpdateEmailForm + include ActiveModel::Model + + attr_accessor :email + + validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } +end diff --git a/pfml/app/forms/users/verify_account_form.rb b/pfml/app/forms/users/verify_account_form.rb new file mode 100644 index 0000000..7508f1e --- /dev/null +++ b/pfml/app/forms/users/verify_account_form.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Users::VerifyAccountForm + include ActiveModel::Model + + attr_accessor :email, :code + + validates :email, :code, presence: true + validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, if: -> { email.present? } + validates :code, length: { is: 6 }, if: -> { code.present? } +end diff --git a/pfml/app/helpers/users/mfa_helper.rb b/pfml/app/helpers/users/mfa_helper.rb new file mode 100644 index 0000000..7b291c7 --- /dev/null +++ b/pfml/app/helpers/users/mfa_helper.rb @@ -0,0 +1,17 @@ +module MfaHelper + # Generate a QR code for setting up the TOTP device + def totp_qr_code(secret, email) + RQRCode::QRCode.new(totp_qr_code_uri(secret, email)).as_svg( + offset: 0, + color: "000", + shape_rendering: "crispEdges", + module_size: 3, + standalone: true + ) + end + + private + def totp_qr_code_uri(secret, email) + "otpauth://totp/#{email}?secret=#{secret}" + end +end diff --git a/pfml/app/policies/application_policy.rb b/pfml/app/policies/application_policy.rb new file mode 100644 index 0000000..affa276 --- /dev/null +++ b/pfml/app/policies/application_policy.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# This is the parent policy that all other policies should inherit from. +# By default, it prevents all actions (principle of least privilege). +class ApplicationPolicy + attr_reader :user, :record + + def initialize(user, record) + raise Pundit::NotAuthorizedError, "must be logged in" unless user + @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) + raise Pundit::NotAuthorizedError, "must be logged in" unless user + @user = user + @scope = scope + end + + def resolve + raise NotImplementedError, "You must define #resolve in #{self.class}" + end + + private + + attr_reader :user, :scope + end +end diff --git a/pfml/app/services/auth_service.rb b/pfml/app/services/auth_service.rb new file mode 100644 index 0000000..9bcab30 --- /dev/null +++ b/pfml/app/services/auth_service.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +class AuthService + def initialize(auth_adapter = Auth::CognitoAdapter.new) + @auth_adapter = auth_adapter + end + + # Send a confirmation code that's required to change the user's password + def forgot_password(email) + @auth_adapter.forgot_password(email) + end + + def confirm_forgot_password(email, code, password) + @auth_adapter.confirm_forgot_password(email, code, password) + end + + def change_email(uid, new_email) + @auth_adapter.change_email(uid, new_email) + + user = User.find_by(uid: uid) + user.update!(email: new_email) + end + + # Initiate a login for the user. The response will indicate whether the user + # has additional steps, like multi-factor auth, to complete the login. + def initiate_auth(email, password) + response = @auth_adapter.initiate_auth(email, password) + handle_auth_result(response, email) + end + + # Respond to a multi-factor auth challenge + def respond_to_auth_challenge(code, challenge = {}) + response = @auth_adapter.respond_to_auth_challenge(code, challenge) + handle_auth_result(response, challenge[:email]) + end + + def register(email, password, role) + # @TODO: Handle errors from the auth service, like when the email is already taken + account = @auth_adapter.create_account(email, password) + + create_db_user(account[:uid], email, account[:provider], role) + end + + # Verify the code sent to the user as part of their initial sign up process. + # This needs done before they can log in. + def verify_account(email, code) + @auth_adapter.verify_account(email, code) + end + + # Resend the code used for verifying the user's email address + def resend_verification_code(email) + @auth_adapter.resend_verification_code(email) + end + + # Initiate the process of enabling authenticator-app MFA + def associate_software_token(access_token) + @auth_adapter.associate_software_token(access_token) + end + + # Complete the process of enabling authenticator-app MFA + def verify_software_token(code, user) + @auth_adapter.verify_software_token(code, user.access_token) + user.update!(mfa_preference: "software_token") + end + + # Disable authenticator-app MFA for the user + def disable_software_token(user) + @auth_adapter.disable_software_token(user.uid) + user.update!(mfa_preference: "opt_out") + end + + private + + def create_db_user(uid, email, provider, role = "claimant") + Rails.logger.info "Creating User uid: #{uid}, and UserRole: #{role}" + + user = User.create!( + uid: uid, + email: email, + provider: provider, + ) + user_role = UserRole.create!(user: user, role: role) + user + end + + def handle_auth_result(response, email) + unless response[:uid] + return response + end + + user = User.find_by(uid: response[:uid]) + + if user.nil? + user = create_db_user( + response[:uid], + email, + response[:provider] + ) + elsif user.email != email + # If the user's email changed outside of our system, then sync the changes + user.update!(email: email) + end + + { + access_token: response[:access_token], + user: user + } + end +end diff --git a/pfml/app/views/application/_breadcrumbs.html.erb b/pfml/app/views/application/_breadcrumbs.html.erb new file mode 100644 index 0000000..63dc4b7 --- /dev/null +++ b/pfml/app/views/application/_breadcrumbs.html.erb @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/pfml/app/views/application/_flash.html.erb b/pfml/app/views/application/_flash.html.erb new file mode 100644 index 0000000..2a9fbb4 --- /dev/null +++ b/pfml/app/views/application/_flash.html.erb @@ -0,0 +1,45 @@ +<% # @TODO: Dry this up using a partial or something like View Components %> +<% # https://api.rubyonrails.org/classes/ActionDispatch/Flash.html %> +<% if flash[:notice] || flash[:errors] || alert || notice %> +
+
+ <% if flash[:notice] || notice %> +
+
+

<%= flash[:notice] || notice %>

+
+
+ <% end %> + + <% if flash[:errors] || alert %> + + <% end %> +
+
+<% end %> \ No newline at end of file diff --git a/pfml/app/views/application/_header.html.erb b/pfml/app/views/application/_header.html.erb new file mode 100644 index 0000000..6a61cc3 --- /dev/null +++ b/pfml/app/views/application/_header.html.erb @@ -0,0 +1,40 @@ +
+
+
+
+ + + +
+ + + + <%= render partial: "language-toggle", locals: { container_class: "usa-language-container display-none desktop:display-block" } %> +
+
\ No newline at end of file diff --git a/pfml/app/views/application/_language-toggle.html.erb b/pfml/app/views/application/_language-toggle.html.erb new file mode 100644 index 0000000..89fa0d4 --- /dev/null +++ b/pfml/app/views/application/_language-toggle.html.erb @@ -0,0 +1,11 @@ +<% inactive_locale = I18n.locale == :"en" ? :"es-US" : :"en" %> + +
+ <%= + link_to t('header.language_toggle', locale: inactive_locale), + url_for(locale: inactive_locale.to_s), + hreflang: inactive_locale, + rel: 'alternate', + class: 'usa-button usa-button--outline padding-y-1 height-auto' + %> +
\ No newline at end of file diff --git a/pfml/app/views/application/sandbox.html.erb b/pfml/app/views/application/sandbox.html.erb new file mode 100644 index 0000000..a96e774 --- /dev/null +++ b/pfml/app/views/application/sandbox.html.erb @@ -0,0 +1,44 @@ +<%# This page is for development purposes and is only available during local development %> + +

Sandbox

+ +

Notifications

+ +<%= us_form_with url: dev_send_email_path do |f| %> + <%= f.email_field :email %> + <%= f.submit 'Send' %> +<% end %> + +
+ +

us_form_with

+ +<%= us_form_with url: authenticate_user_path do |f| %> + <%= f.text_field :username, { label: "Name", hint: 'As shown on your legal ID'} %> + <%= f.email_field :email, { class: 'usa-input--md' } %> + <%= f.password_field :password, { hint: "Test hint" } %> + + <%= f.fieldset "What's your age?" do %> + <%= f.radio_button :age, "child", { label: "I am younger than 21" } %> + <%= f.radio_button :age, "adult", { label: "I am over 21" } %> + <% end %> + + <%= f.yes_no :has_previous_leave, { + yes_options: { label: "Yes, I've taken leave before", hint: "You may be eligible for additional benefits" }, + no_options: { label: "No, I haven't taken leave before" } + } %> + + <%= f.fieldset "What pets do you have?" do %> + <%= f.hint "Fieldset hint text here" %> + <%= f.check_box :pet_dog, { label: "I own a dog" } %> + <%= f.check_box :pet_cat, { label: "I own a cat", hint: "They have nine lives"} %> + <% end %> + + <%= f.select :city, ["Berlin", "Chicago", "Madrid"] %> + + <%= f.text_area :message %> + + <%= f.file_field :picture %> + + <%= f.submit 'Save and continue' %> +<% end %> \ No newline at end of file diff --git a/pfml/app/views/home/index.html.erb b/pfml/app/views/home/index.html.erb new file mode 100644 index 0000000..3094976 --- /dev/null +++ b/pfml/app/views/home/index.html.erb @@ -0,0 +1,72 @@ +<% content_for :title, t(".title") %> +<%= content_for :main_col_class, "bg-base-lightest" %> + +

+ <%= t(".title") %> +

+

+ <%= t('.intro') %> +

+ +
+ +
\ No newline at end of file diff --git a/pfml/app/views/users/accounts/edit.html.erb b/pfml/app/views/users/accounts/edit.html.erb new file mode 100644 index 0000000..ba16de2 --- /dev/null +++ b/pfml/app/views/users/accounts/edit.html.erb @@ -0,0 +1,35 @@ +<% content_for :title, t('.title') %> + +

<%= t('.title') %>

+ +
+
+

<%= t('.change_email') %>

+ <%= us_form_with model: @email_form, url: users_account_email_path, method: :patch do |f| %> + <%= f.email_field :email %> + <%= f.submit t('.change_email') %> + <% end %> +
+ +
+

<%= t('.change_password') %>

+

<%= t('.change_password_instructions') %>

+ <%= us_form_with model: @password_form, url: users_forgot_password_path, local: true do |f| %> + <%= f.hidden_field :email %> + <%= f.submit t('.change_password'), { class: "margin-top-0" } %> + <% end %> +
+ +
+

<%= t('.mfa_preference') %>

+ +

+ <% if current_user.mfa_preference == "software_token" %> + <%= button_to users_disable_mfa_path, { method: :delete, class: "usa-button usa-button--unstyled" } do %> + <%= t('.disable_mfa') %> + <% end %> + <% else %> + <%= link_to t(".enable_mfa"), new_users_mfa_path %> + <% end %> +

+
\ No newline at end of file diff --git a/pfml/app/views/users/mfa/new.html.erb b/pfml/app/views/users/mfa/new.html.erb new file mode 100644 index 0000000..2428753 --- /dev/null +++ b/pfml/app/views/users/mfa/new.html.erb @@ -0,0 +1,50 @@ +<% content_for :title, t('.title') %> + +

<%= t('.title') %>

+

<%= t('.lead') %>

+ +
    +
  1. +

    <%= t('.open_app') %>

    + +
    +

    + +

    +
    +

    + <%= t('.auth_apps_html') %> +

    +
    +
    +
  2. + +
  3. +

    <%= t('.qr_heading') %>

    + +
    <%= totp_qr_code(@secret_code, @email).html_safe %>
    + +
    <%= t('.code_label') %>
    + <%= @secret_code %> +
  4. + +
  5. +

    <%= t('.temporary_code') %>

    + + <%= form_with model: @form, url: users_mfa_index_path, local: true, html: { class: "usa-form" } do |f| %> + <%= f.text_field :temporary_code, { + autocomplete: "one-time-code", + class: "usa-input usa-input--md", + aria: { labelledby: "temporary_code_heading" } + } %> + + <%= f.submit nil, class: "usa-button" %> + <% end %> +
  6. +
\ No newline at end of file diff --git a/pfml/app/views/users/mfa/preference.html.erb b/pfml/app/views/users/mfa/preference.html.erb new file mode 100644 index 0000000..5c14f32 --- /dev/null +++ b/pfml/app/views/users/mfa/preference.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, t('.title') %> + +

<%= t('.title') %>

+ +<%= us_form_with model: @form, url: users_update_mfa_preference_path, local: true do |f| %> + <%= f.fieldset t(".preference") do %> + <%= f.radio_button :mfa_preference, :software_token, { label: t('.software_token_label'), hint: t('.software_token_hint') } %> + <%= f.radio_button :mfa_preference, :opt_out, { label: t('.opt_out_label'), hint: t('.opt_out_hint') } %> + <% end %> + + <%= f.submit %> +<% end %> \ No newline at end of file diff --git a/pfml/app/views/users/passwords/forgot.html.erb b/pfml/app/views/users/passwords/forgot.html.erb new file mode 100644 index 0000000..6efd207 --- /dev/null +++ b/pfml/app/views/users/passwords/forgot.html.erb @@ -0,0 +1,10 @@ +<% content_for :title, t(".title") %> + +

<%= t(".title") %>

+

<%= t(".instructions") %>

+ +<%= us_form_with model: @form, url: users_forgot_password_path, local: true do |f| %> + <%= f.email_field :email, { autocomplete: "username" } %> + + <%= f.submit t(".submit") %> +<% end %> \ No newline at end of file diff --git a/pfml/app/views/users/passwords/reset.html.erb b/pfml/app/views/users/passwords/reset.html.erb new file mode 100644 index 0000000..cf187f2 --- /dev/null +++ b/pfml/app/views/users/passwords/reset.html.erb @@ -0,0 +1,31 @@ +<% content_for :title, t(".title") %> + +
+
+

<%= t(".instructions_heading") %>

+

+ <%= t(".instructions_html", verify_account_url: url_for(users_verify_account_path)) %> +

+
+
+ +

<%= t(".title") %>

+ +<%= us_form_with model: @form, url: users_reset_password_path, local: true do |f| %> + <%= f.text_field :code, { autocomplete: "off", label: t('.code'), width: "md" } %> + + <%= f.email_field :email, { autocomplete: "username" } %> + + <%= f.password_field :password, autocomplete: "new-password", id: "new-password", hint: t("users.password_hint") %> + + + + <%= f.submit t(".submit") %> +<% end %> \ No newline at end of file diff --git a/pfml/app/views/users/registrations/new.html.erb b/pfml/app/views/users/registrations/new.html.erb new file mode 100644 index 0000000..f7da682 --- /dev/null +++ b/pfml/app/views/users/registrations/new.html.erb @@ -0,0 +1,64 @@ +<% content_for :title, t(".title_#{@form.role}") %> +<% icon = @form.role == "claimant" ? "local_library" : "local_library" %> +<% icon_color = @form.role == "claimant" ? "violet" : "mint" %> + +
+
+ + +

<%= t(".title_#{@form.role}") %>

+
+ + <%= us_form_with model: @form, url: users_registrations_path, local: true do |f| %> + <%= f.hidden_field :role %> + <%= f.email_field :email %> + + <%= f.password_field :password, autocomplete: "new-password", id: "new-password", hint: t("users.password_hint") %> + + + + <%= f.password_field :password_confirmation, autocomplete: "new-password", id: "new-password-confirmation" %> + + <%= f.submit t(".title_#{@form.role}") %> + <% end %> +
+ +

+ <%= t ".have_existing_account" %> + + <%= t ".login" %> + +

+ +<%= content_for :sidebar do %> + <% if @form.role == "claimant" %> +

<%= t('.are_employer_heading') %>

+
+

<%= t('.are_employer_body') %>

+

+ + <%= t('.are_employer_action') %> + +

+
+ <% else %> +

<%= t('.are_claimant_heading') %>

+
+

<%= t('.are_claimant_body') %>

+

+ + <%= t('.are_claimant_action') %> + +

+
+ <% end %> +<% end %> \ No newline at end of file diff --git a/pfml/app/views/users/registrations/new_account_verification.html.erb b/pfml/app/views/users/registrations/new_account_verification.html.erb new file mode 100644 index 0000000..973bace --- /dev/null +++ b/pfml/app/views/users/registrations/new_account_verification.html.erb @@ -0,0 +1,26 @@ +<% content_for :title, t(".title") %> + +
+

+ <%= t(".title") %> +

+

+ <%= t(".instructions") %> +

+ + <%= us_form_with model: @form, url: users_verify_account_path, local: true do |f| %> + <%= f.email_field :email, { autocomplete: "username", "data-action": "input->registrations#updateResendEmail" } %> + + <%= f.text_field :code, { autocomplete: "off", label: t('.code'), width: "md" } %> + +
+ <%= f.button t(".resend"), { class: "usa-button usa-button--unstyled margin-top-1", form: "resend-form" } %> +
+ + <%= f.submit t(".title") %> + <% end %> + + <%= us_form_with model: @resend_verification_form, url: users_resend_verification_path, local: true, id: "resend-form" do |f| %> + <%= f.hidden_field :email, { "data-registrations-target": "resendEmailField" } %> + <% end %> +
\ No newline at end of file diff --git a/pfml/app/views/users/sessions/challenge.html.erb b/pfml/app/views/users/sessions/challenge.html.erb new file mode 100644 index 0000000..608c8ce --- /dev/null +++ b/pfml/app/views/users/sessions/challenge.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, t(".title") %> + +
+

<%= t(".title") %>

+ + <%= us_form_with model: @form, url: respond_to_session_challenge_path, local: true do |f| %> + <%= f.text_field :code, { autocomplete: "one-time-code", label: t(".code"), width: "md" } %> + + <%= f.submit t(".submit") %> + <% end %> +
diff --git a/pfml/app/views/users/sessions/new.html.erb b/pfml/app/views/users/sessions/new.html.erb new file mode 100644 index 0000000..d0cb0f2 --- /dev/null +++ b/pfml/app/views/users/sessions/new.html.erb @@ -0,0 +1,34 @@ +<% content_for :title, t(".title") %> + +
+

+ <%= t(".title") %> +

+ + <%= us_form_with model: @form, url: new_user_session_path, local: true do |f| %> + <%= f.email_field :email, { autocomplete: "username" } %> + + <%= f.password_field :password, {id: "password", autocomplete: "current-password"} %> + + + <%= f.submit t(".title") %> + +

+ <%= link_to t('.forgot_password'), users_forgot_password_path %> +

+ <% end %> +
+ +

+ <%= t('.no_account') %> + + <%= t('.create_account') %> + . +

\ No newline at end of file diff --git a/pfml/spec/adapters/cognito_adapter_spec.rb b/pfml/spec/adapters/cognito_adapter_spec.rb new file mode 100644 index 0000000..cf1ebdf --- /dev/null +++ b/pfml/spec/adapters/cognito_adapter_spec.rb @@ -0,0 +1,153 @@ +require "rails_helper" + +RSpec.describe Auth::CognitoAdapter do + let(:mock_client) { instance_double("Aws::CognitoIdentityProvider::Client") } + let(:adapter) { Auth::CognitoAdapter.new(client: mock_client) } + let(:email) { "test@example.com" } + + describe "#associate_software_token" do + it "returns the secret_code" do + secret_code = "123456abcdef" + allow(mock_client).to receive(:associate_software_token).and_return( + Aws::CognitoIdentityProvider::Types::AssociateSoftwareTokenResponse.new( + secret_code: secret_code + ) + ) + + response = adapter.associate_software_token("access_token") + + expect(response).to eq(secret_code) + end + + it "raises a provider error if cognito raises an unhandled service error" do + allow(mock_client).to receive(:associate_software_token).and_raise( + Aws::CognitoIdentityProvider::Errors::TooManyRequestsException.new(nil, "mock msg") + ) + + expect do + adapter.associate_software_token("access_token") + end.to raise_error(Auth::Errors::ProviderError) + end + end + + describe "#create_account" do + it "raises an error if the email is already taken" do + allow(mock_client).to receive(:sign_up).and_raise(Aws::CognitoIdentityProvider::Errors::UsernameExistsException.new(nil, "mock msg")) + + expect do + adapter.create_account(email, "password") + end.to raise_error(Auth::Errors::UsernameExists) + end + + it "raises an error if the password is invalid" do + allow(mock_client).to receive(:sign_up).and_raise(Aws::CognitoIdentityProvider::Errors::InvalidPasswordException.new(nil, "mock msg")) + + expect do + adapter.create_account(email, "password") + end.to raise_error(Auth::Errors::InvalidPasswordFormat) + end + + it "raises an invalid password error if cognito raises an invalid parameter exception" do + allow(mock_client).to receive(:sign_up).and_raise(Aws::CognitoIdentityProvider::Errors::InvalidParameterException.new(nil, "mock msg")) + + expect do + adapter.create_account(email, "a") + end.to raise_error(Auth::Errors::InvalidPasswordFormat) + end + + it "raises a provider error if cognito raises an unhandled service error" do + allow(mock_client).to receive(:sign_up).and_raise(Aws::CognitoIdentityProvider::Errors::TooManyRequestsException.new(nil, "mock msg")) + + expect do + adapter.create_account(email, "password") + end.to raise_error(Auth::Errors::ProviderError) + end + end + + describe "#initiate_auth" do + it "raises an error if the password is incorrect" do + allow(mock_client).to receive(:admin_initiate_auth).and_raise(Aws::CognitoIdentityProvider::Errors::NotAuthorizedException.new(nil, "mock msg")) + + expect do + adapter.initiate_auth(email, "password") + end.to raise_error(Auth::Errors::InvalidCredentials) + end + + it "raises a provider error if cognito raises an unhandled service error" do + allow(mock_client).to receive(:admin_initiate_auth).and_raise(Aws::CognitoIdentityProvider::Errors::TooManyRequestsException.new(nil, "mock msg")) + + expect do + adapter.initiate_auth(email, "password") + end.to raise_error(Auth::Errors::ProviderError) + end + end + + describe "#forgot_password" do + it "responds with the confirmation channel" do + allow(mock_client).to receive(:forgot_password).and_return( + Aws::CognitoIdentityProvider::Types::ForgotPasswordResponse.new( + code_delivery_details: Aws::CognitoIdentityProvider::Types::CodeDeliveryDetailsType.new(delivery_medium: "EMAIL") + ) + ) + response = adapter.forgot_password(email) + + expect(response).to eq({ confirmation_channel: "EMAIL" }) + end + end + + describe "#verify_software_token" do + it "sets the MFA preference when the token is verified" do + allow(mock_client).to receive(:verify_software_token).and_return( + Aws::CognitoIdentityProvider::Types::VerifySoftwareTokenResponse.new( + status: "SUCCESS" + ) + ) + allow(mock_client).to receive(:set_user_mfa_preference).and_return( + Aws::CognitoIdentityProvider::Types::SetUserMFAPreferenceResponse.new + ) + + adapter.verify_software_token("123456", "mock_token") + + expect(mock_client).to have_received(:set_user_mfa_preference).with( + access_token: "mock_token", + software_token_mfa_settings: { + enabled: true, + preferred_mfa: true + } + ) + end + + it "raises an error if the response status isn't success" do + allow(mock_client).to receive(:verify_software_token).and_return( + Aws::CognitoIdentityProvider::Types::VerifySoftwareTokenResponse.new( + status: "FAILED" + ) + ) + + expect do + adapter.verify_software_token("123456", "mock_token") + end.to raise_error(Auth::Errors::InvalidSoftwareToken) + end + + + it "raises an error when EnableSoftwareTokenMFAException is raised" do + allow(mock_client).to receive(:verify_software_token).and_raise( + Aws::CognitoIdentityProvider::Errors::EnableSoftwareTokenMFAException.new(nil, "mock msg") + ) + + expect do + adapter.verify_software_token("123456", "mock_token") + end.to raise_error(Auth::Errors::InvalidSoftwareToken) + end + + it "raises a provider error when cognito raises an unhandled service error" do + allow(mock_client).to receive(:verify_software_token).and_raise( + Aws::CognitoIdentityProvider::Errors::TooManyRequestsException.new(nil, "mock msg") + ) + + expect do + adapter.verify_software_token("123456", "mock_token") + end.to raise_error(Auth::Errors::ProviderError) + end + end +end diff --git a/pfml/spec/controllers/home_controller_spec.rb b/pfml/spec/controllers/home_controller_spec.rb new file mode 100644 index 0000000..46f1a2d --- /dev/null +++ b/pfml/spec/controllers/home_controller_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +RSpec.describe HomeController do + render_views + + describe "GET index" do + it "renders the index template" do + get :index, params: { locale: "en" } + + expect(response.body).to have_selector("h1", text: /get started/i) + end + end +end diff --git a/pfml/spec/controllers/users/accounts_controller_spec.rb b/pfml/spec/controllers/users/accounts_controller_spec.rb new file mode 100644 index 0000000..4294b57 --- /dev/null +++ b/pfml/spec/controllers/users/accounts_controller_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +RSpec.describe Users::AccountsController do + render_views + + before do + allow(controller).to receive(:auth_service).and_return( + AuthService.new(Auth::MockAdapter.new) + ) + end + + describe "GET edit" do + it "renders the account edit form" do + user = create(:user) + sign_in user + + get :edit, params: { locale: "en" } + + expect(response.body).to have_field("users_update_email_form[email]", with: user.email) + end + + it "shows disable MFA button if MFA is enabled" do + user = create(:user, mfa_preference: "software_token") + sign_in user + + get :edit, params: { locale: "en" } + + expect(response.body).to have_element("button", text: /disable multi-factor/i) + end + + it "shows enable MFA button if MFA is disabled" do + user = create(:user, mfa_preference: "opt_out") + sign_in user + + get :edit, params: { locale: "en" } + + expect(response.body).to have_element("a", text: /enable multi-factor/i) + end + end + + describe "PATCH update_email" do + it "updates the user's email" do + new_email = "new@example.com" + user = create(:user) + sign_in user + + patch :update_email, params: { + users_update_email_form: { email: new_email }, + locale: "en" + } + user.reload + + expect(user.email).to eq(new_email) + end + + it "validates the email" do + user = create(:user) + sign_in user + + patch :update_email, params: { + users_update_email_form: { email: "invalid-email" }, + locale: "en" + } + + expect(response.status).to eq(422) + end + end +end diff --git a/pfml/spec/controllers/users/mfa_controller_spec.rb b/pfml/spec/controllers/users/mfa_controller_spec.rb new file mode 100644 index 0000000..5caf116 --- /dev/null +++ b/pfml/spec/controllers/users/mfa_controller_spec.rb @@ -0,0 +1,136 @@ +require 'rails_helper' + +RSpec.describe Users::MfaController do + render_views + + before do + allow(controller).to receive(:auth_service).and_return( + AuthService.new(Auth::MockAdapter.new) + ) + end + + describe "preference" do + it "renders the MFA preference form" do + user = create(:user) + sign_in user + + get :preference, params: { locale: "en" } + + expect(response.body).to have_selector("h1", text: /multi-factor authentication/i) + end + end + + describe "update_preference" do + it "updates the MFA preference for the user when opting out" do + user = create(:user, mfa_preference: nil) + sign_in user + + patch :update_preference, params: { + users_mfa_preference_form: { mfa_preference: "opt_out" }, + locale: "en" + } + + expect(user.mfa_preference).to be_nil + user.reload + expect(user.mfa_preference).to eq("opt_out") + + # Redirect path likely to change once Claimant portal is implemented + expect(response).to redirect_to(users_account_path) + end + + it "does not set preference when selecting software token" do + user = create(:user, mfa_preference: nil) + sign_in user + + patch :update_preference, params: { + users_mfa_preference_form: { mfa_preference: "software_token" }, + locale: "en" + } + + user.reload + expect(user.mfa_preference).to be_nil + expect(response).to redirect_to(action: :new) + end + end + + describe "new" do + it "renders the MFA setup form" do + user = create(:user, access_token: JWT.encode({ exp: 1.day.from_now.to_i }, nil)) + sign_in user + + get :new, params: { locale: "en" } + + expect(response.body).to have_selector("h1", text: /add an authentication app/i) + expect(response.body).to have_text("mock-secret") + end + + it "redirects to the login page if the access token is close to expiring" do + user = create(:user, access_token: JWT.encode({ exp: 5.minutes.from_now.to_i }, nil)) + sign_in user + + get :new, params: { locale: "en" } + + expect(response).to redirect_to(new_user_session_path) + end + end + + describe "create" do + it "associates the software token with the user" do + user = create( + :user, + mfa_preference: nil, + access_token: JWT.encode({ exp: 1.day.from_now.to_i }, nil) + ) + sign_in user + + post :create, params: { + users_associate_mfa_form: { temporary_code: "123123" }, + locale: "en" + } + + expect(user.mfa_preference).not_to eq("software_token") + user.reload + expect(user.mfa_preference).to eq("software_token") + + expect(response).to redirect_to(root_path) + end + + it "redirects back to the setup page if the code is invalid" do + user = create(:user, access_token: JWT.encode({ exp: 1.day.from_now.to_i }, nil)) + sign_in user + + post :create, params: { + users_associate_mfa_form: { temporary_code: "wrong format" }, + locale: "en" + } + + expect(flash[:errors]).to include(/wrong length/i) + expect(response).to redirect_to(action: :new) + end + + it "redirects back to the setup page if the code is incorrect" do + user = create(:user, access_token: JWT.encode({ exp: 1.day.from_now.to_i }, nil)) + sign_in user + + post :create, params: { + users_associate_mfa_form: { temporary_code: "000001" }, + locale: "en" + } + + expect(flash[:errors]).to include(/invalid code/i) + expect(response).to redirect_to(action: :new) + end + end + + describe "destroy" do + it "disables MFA for the user" do + user = create(:user, mfa_preference: "software_token") + sign_in user + + delete :destroy, params: { locale: "en" } + + user.reload + expect(user.mfa_preference).to eq("opt_out") + end + end +end diff --git a/pfml/spec/controllers/users/passwords_controller_spec.rb b/pfml/spec/controllers/users/passwords_controller_spec.rb new file mode 100644 index 0000000..ce0895c --- /dev/null +++ b/pfml/spec/controllers/users/passwords_controller_spec.rb @@ -0,0 +1,99 @@ +require 'rails_helper' + +RSpec.describe Users::PasswordsController do + render_views + + before do + allow(controller).to receive(:auth_service).and_return( + AuthService.new(Auth::MockAdapter.new) + ) + end + + describe "GET forgot" do + it "renders the forgot password form" do + get :forgot, params: { locale: "en" } + + expect(response.body).to have_field("users_forgot_password_form[email]") + end + end + + describe "POST send_reset_password_instructions" do + it "redirects to the reset password form" do + post :send_reset_password_instructions, params: { + users_forgot_password_form: { email: "test@example.com" }, + locale: "en" + } + + expect(response).to redirect_to(users_reset_password_path) + end + + it "validates email" do + post :send_reset_password_instructions, params: { + users_forgot_password_form: { email: "invalid" }, + locale: "en" + } + + expect(response.status).to eq(422) + end + + it "handles auth provider errors" do + post :send_reset_password_instructions, params: { + users_forgot_password_form: { email: "UsernameExists@example.com" }, + locale: "en" + } + + expect(response.status).to eq(422) + end + end + + describe "GET reset" do + it "renders the reset password form" do + get :reset, params: { locale: "en" } + + expect(response.body).to have_field("users_reset_password_form[email]") + expect(response.body).to have_field("users_reset_password_form[code]") + expect(response.body).to have_field("users_reset_password_form[password]") + end + end + + describe "POST confirm_reset" do + it "redirects to the login page" do + post :confirm_reset, params: { + users_reset_password_form: { + email: "test@example.com", + code: "123456", + password: "password" + }, + locale: "en" + } + + expect(response).to redirect_to(new_user_session_path) + end + + it "validates email and code" do + post :confirm_reset, params: { + users_reset_password_form: { + email: "invalid", + code: "123456", + password: "password" + }, + locale: "en" + } + + expect(response.status).to eq(422) + end + + it "handles auth provider errors" do + post :confirm_reset, params: { + users_reset_password_form: { + email: "test@example.com", + code: "000001", + password: "password" + }, + locale: "en" + } + + expect(response.status).to eq(422) + end + end +end diff --git a/pfml/spec/controllers/users/registrations_controller_spec.rb b/pfml/spec/controllers/users/registrations_controller_spec.rb new file mode 100644 index 0000000..4ce7fe9 --- /dev/null +++ b/pfml/spec/controllers/users/registrations_controller_spec.rb @@ -0,0 +1,121 @@ +require 'rails_helper' + +RSpec.describe Users::RegistrationsController do + render_views + + before do + allow(controller).to receive(:auth_service).and_return( + AuthService.new(Auth::MockAdapter.new) + ) + end + + describe "GET new_claimant" do + it "renders with claimant content and role" do + get :new_claimant, params: { locale: "en" } + + expect(response.body).to have_selector("h1", text: /create a claimant account/i) + expect(response.body).to have_field("users_registration_form[role]", with: "claimant", type: :hidden) + end + end + + describe "GET new_employer" do + it "renders with employer content and role" do + get :new_employer, params: { locale: "en" } + + expect(response.body).to have_selector("h1", text: /create an employer account/i) + expect(response.body).to have_field("users_registration_form[role]", with: "employer", type: :hidden) + end + end + + describe "POST create" do + it "creates a new user and routes to verify page" do + email = "test@example.com" + + post :create, params: { + users_registration_form: { + email: email, + password: "password", + role: "employer" + }, + locale: "en" + } + user = User.find_by(email: email) + + expect(user).to be_present + expect(user.employer?).to be(true) + expect(response).to redirect_to(users_verify_account_path) + end + + it "validates email" do + post :create, params: { + users_registration_form: { + email: "invalid", + password: "password", + role: "employer" + }, + locale: "en" + } + + expect(response.status).to eq(422) + end + + it "handles auth provider errors" do + post :create, params: { + users_registration_form: { + email: "UsernameExists@example.com", + password: "password", + role: "employer" + }, + locale: "en" + } + + expect(response.status).to eq(422) + end + end + + describe "GET new_account_verification" do + it "renders the account verification form" do + get :new_account_verification, params: { locale: "en" } + + expect(response.body).to have_field("users_verify_account_form[code]") + end + end + + describe "POST create_account_verification" do + it "redirects to sign in" do + post :create_account_verification, params: { + users_verify_account_form: { + email: "test@example.com", + code: "123456" + }, + locale: "en" + } + + expect(response).to redirect_to(new_user_session_path) + end + + it "validates email" do + post :create_account_verification, params: { + users_verify_account_form: { + email: "invalid", + code: "123456" + }, + locale: "en" + } + + expect(response.status).to eq(422) + end + + it "handles auth provider errors" do + post :create_account_verification, params: { + users_verify_account_form: { + email: "test@example.com", + code: "000001" + }, + locale: "en" + } + + expect(response.status).to eq(422) + end + end +end diff --git a/pfml/spec/controllers/users/sessions_controller_spec.rb b/pfml/spec/controllers/users/sessions_controller_spec.rb new file mode 100644 index 0000000..9baaaec --- /dev/null +++ b/pfml/spec/controllers/users/sessions_controller_spec.rb @@ -0,0 +1,144 @@ +require 'rails_helper' + +RSpec.describe Users::SessionsController do + render_views + + # Hard-code so we can reliably connect the session to a test user we create + let (:uid) { "mock-uid" } + let (:uid_generator) { -> { uid } } + + before do + @request.env["devise.mapping"] = Devise.mappings[:user] + + allow(controller).to receive(:auth_service).and_return( + AuthService.new(Auth::MockAdapter.new(uid_generator: uid_generator)) + ) + end + + describe "GET new" do + it "renders the login form" do + get :new, params: { locale: "en" } + + expect(response.body).to have_selector("h1", text: /sign in/i) + end + end + + describe "POST create" do + it "renders the form with errors if credentials are wrong" do + post :create, params: { + users_new_session_form: { + email: "text@example.com", + password: "wrong" + }, + locale: "en" + } + + expect(response.status).to eq(422) + expect(response.body).to have_selector("h1", text: /sign in/i) + expect(response.body).to have_selector(".usa-alert--error") + end + + it "signs in a claimant and redirects to their account page (for now)" do + create(:user, uid: uid) + + post :create, params: { + users_new_session_form: { + email: "test@example.com", + password: "password" + }, + locale: "en" + } + + expect(response).to redirect_to(users_account_path) + end + + it "signs in and redirects to MFA preference page if a preference is not set" do + post :create, params: { + users_new_session_form: { + email: "test@example.com", + password: "password" + }, + locale: "en" + } + + expect(response).to redirect_to(users_mfa_preference_path) + end + + it "redirects to the verify account page if the user is not confirmed" do + post :create, params: { + users_new_session_form: { + email: "unconfirmed@example.com", + password: "password" + }, + locale: "en" + } + + expect(response).to redirect_to(users_verify_account_path) + end + + it "redirects to the challenge page if MFA is required" do + create(:user, uid: uid, mfa_preference: nil) + + post :create, params: { + users_new_session_form: { + email: "mfa@example.com", + password: "password" + }, + locale: "en" + } + + expect(session[:challenge_session]).to eq("mock-session") + expect(session[:challenge_email]).to eq("mfa@example.com") + expect(response).to redirect_to(session_challenge_path) + end + end + + describe "GET challenge" do + it "renders the MFA challenge form" do + session[:challenge_session] = "session" + session[:challenge_email] = "test@example.com" + + get :challenge, params: { locale: "en" } + + expect(response.body).to have_selector("h1", text: /enter your authentication app code/i) + end + + it "redirects to the login page if there is no challenge session" do + get :challenge, params: { locale: "en" } + + expect(response).to redirect_to(new_user_session_path) + end + end + + describe "POST respond_to_challenge" do + it "renders the form with errors if the code is invalid" do + session[:challenge_session] = "session" + session[:challenge_email] = "test@example.com" + + post :respond_to_challenge, params: { + users_auth_app_code_form: { + code: "wrong" + }, + locale: "en" + } + + expect(response.status).to eq(422) + expect(response.body).to have_selector(".usa-alert--error") + end + + it "signs in a claimant and redirects to their account page (for now)" do + create(:user, uid: uid) + session[:challenge_session] = "session" + session[:challenge_email] = "test@example.com" + + post :respond_to_challenge, params: { + users_auth_app_code_form: { + code: "123456" + }, + locale: "en" + } + + expect(response).to redirect_to(users_account_path) + end + end +end diff --git a/pfml/spec/factories/user_role_factory.rb b/pfml/spec/factories/user_role_factory.rb new file mode 100644 index 0000000..645a719 --- /dev/null +++ b/pfml/spec/factories/user_role_factory.rb @@ -0,0 +1,18 @@ +FactoryBot.define do + factory :user_role do + user + + trait :claimant do + role { "claimant" } + end + + trait :employer do + role { "employer" } + end + + trait :superadmin do + role { "superadmin" } + user { create(:user, :superadmin) } + end + end +end diff --git a/pfml/spec/factories/users_factory.rb b/pfml/spec/factories/users_factory.rb new file mode 100644 index 0000000..5742461 --- /dev/null +++ b/pfml/spec/factories/users_factory.rb @@ -0,0 +1,20 @@ +FactoryBot.define do + factory :user do + email { Faker::Internet.email } + uid { Faker::Internet.uuid } + provider { "factory_bot" } + mfa_preference { "opt_out" } + + trait :claimant do + user_role { create(:user_role, :claimant) } + end + + trait :employer do + user_role { create(:user_role, :employer) } + end + + trait :superadmin do + email { "test+admin@navapbc.com" } + end + end +end diff --git a/pfml/spec/forms/users/new_session_form_spec.rb b/pfml/spec/forms/users/new_session_form_spec.rb new file mode 100644 index 0000000..f9b6107 --- /dev/null +++ b/pfml/spec/forms/users/new_session_form_spec.rb @@ -0,0 +1,32 @@ +require "rails_helper" + +RSpec.describe Users::NewSessionForm do + it "passes validation with valid email and password" do + form = Users::NewSessionForm.new( + email: "test@example.com", + password: "password" + ) + + expect(form).to be_valid + end + + it "requires email and password" do + form = Users::NewSessionForm.new({ + email: "", + password: "" + }) + + expect(form).not_to be_valid + expect(form.errors.of_kind?(:email, :blank)).to be_truthy + expect(form.errors.of_kind?(:password, :blank)).to be_truthy + end + + it "requires a valid email" do + form = Users::NewSessionForm.new( + email: "not_an_email" + ) + + expect(form).not_to be_valid + expect(form.errors.of_kind?(:email, :invalid)).to be_truthy + end +end diff --git a/pfml/spec/forms/users/registration_form_spec.rb b/pfml/spec/forms/users/registration_form_spec.rb new file mode 100644 index 0000000..c9ad0b8 --- /dev/null +++ b/pfml/spec/forms/users/registration_form_spec.rb @@ -0,0 +1,39 @@ +require "rails_helper" + +valid_password = "password1234" + +RSpec.describe Users::RegistrationForm do + let (:form) { Users::RegistrationForm.new(role: "claimant") } + + it "passes validation with valid email and password" do + form.email = "test@example.com" + form.password = valid_password + form.password_confirmation = valid_password + + expect(form).to be_valid + end + + it "requires email and password" do + form.email = "" + form.password = "" + + expect(form).not_to be_valid + expect(form.errors.of_kind?(:email, :blank)).to be_truthy + expect(form.errors.of_kind?(:password, :blank)).to be_truthy + end + + it "confirms the password matches" do + form.password = valid_password + form.password_confirmation = "not_the_same" + + expect(form).not_to be_valid + expect(form.errors.of_kind?(:password_confirmation, :confirmation)).to be_truthy + end + + it "requires a valid email" do + form.email = "not_an_email" + + expect(form).not_to be_valid + expect(form.errors.of_kind?(:email, :invalid)).to be_truthy + end +end diff --git a/pfml/spec/forms/users/verify_account_form_spec.rb b/pfml/spec/forms/users/verify_account_form_spec.rb new file mode 100644 index 0000000..83ed152 --- /dev/null +++ b/pfml/spec/forms/users/verify_account_form_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Users::VerifyAccountForm do + it "passes validation with valid email and code" do + form = Users::VerifyAccountForm.new(email: "test@example.com", code: "123456") + + expect(form).to be_valid + end + + it "requires email and code" do + form = Users::VerifyAccountForm.new(email: nil, code: nil) + + expect(form).to be_invalid + expect(form.errors.of_kind?(:email, :blank)).to be_truthy + expect(form.errors.of_kind?(:code, :blank)).to be_truthy + end + + it "requires email to be a valid email" do + form = Users::VerifyAccountForm.new(email: "invalid-email", code: "123456") + + expect(form).to be_invalid + expect(form.errors.of_kind?(:email, :invalid)).to be_truthy + end + + it "requires code to be 6 characters" do + form = Users::VerifyAccountForm.new(email: "test@example.com", code: "12345") + + expect(form).to be_invalid + expect(form.errors.of_kind?(:code, :wrong_length)).to be_truthy + end +end diff --git a/pfml/spec/models/user_spec.rb b/pfml/spec/models/user_spec.rb new file mode 100644 index 0000000..b958a61 --- /dev/null +++ b/pfml/spec/models/user_spec.rb @@ -0,0 +1,71 @@ +require 'rails_helper' + +RSpec.describe User, type: :model do + describe "access_token" do + let(:user) { build(:user) } + + it "is an attr_accessor so we can store the token in the session" do + expect(user).to respond_to(:access_token) + expect(user).to respond_to(:access_token=) + end + end + + describe "access_token_expires_within_minutes?" do + let(:user) { build(:user) } + let(:access_token) { + JWT.encode({ exp: 5.minutes.from_now.to_i }, nil) + } + + it "returns true if the access token expires within the designated minutes" do + expect(user.access_token_expires_within_minutes?(access_token, 5)).to eq(true) + end + + it "returns false if the access token is not expiring within the designated minutes" do + expect(user.access_token_expires_within_minutes?(access_token, 1)).to eq(false) + end + end + + describe "claimant?" do + let(:user) { build(:user) } + + it "returns true if the user has a claimant role" do + user.user_role = build(:user_role, :claimant) + expect(user.claimant?).to eq(true) + end + + it "returns false if the user does not have a claimant role" do + user.user_role = build(:user_role, :employer) + expect(user.claimant?).to eq(false) + end + end + + describe "employer?" do + let(:user) { build(:user) } + + it "returns true if the user has an employer role" do + user.user_role = build(:user_role, :employer) + expect(user.employer?).to eq(true) + end + + it "returns false if the user does not have an employer role" do + user.user_role = build(:user_role, :claimant) + expect(user.employer?).to eq(false) + end + end + + describe "superadmin?" do + let(:user) { build(:user) } + + pending "returns true for a superadmin" + + it "returns false for a claimant" do + user.user_role = build(:user_role, :claimant) + expect(user.superadmin?).to eq(false) + end + + it "returns false for an employer" do + user.user_role = build(:user_role, :employer) + expect(user.superadmin?).to eq(false) + end + end +end diff --git a/pfml/spec/rails_helper.rb b/pfml/spec/rails_helper.rb new file mode 100644 index 0000000..0dba589 --- /dev/null +++ b/pfml/spec/rails_helper.rb @@ -0,0 +1,74 @@ +# SimpleCov needs to be required before any of your application code is loaded. +require 'simplecov' +SimpleCov.start 'rails' do + add_filter 'lib/templates/' + add_filter 'lib/generators/' +end + +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! +require_relative 'support/factory_bot' + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end +RSpec.configure do |config| + config.include Devise::Test::ControllerHelpers, type: :controller + + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_paths = [ + Rails.root.join('spec/fixtures') + ] + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/6-0/rspec-rails + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/pfml/spec/services/auth_service_spec.rb b/pfml/spec/services/auth_service_spec.rb new file mode 100644 index 0000000..89170d8 --- /dev/null +++ b/pfml/spec/services/auth_service_spec.rb @@ -0,0 +1,62 @@ +require "rails_helper" + +RSpec.describe AuthService do + let(:mock_uid) { "mock_uid" } + let(:mock_auth_adapter) { Auth::MockAdapter.new(uid_generator: -> { mock_uid }) } + + describe "#register" do + it "creates a new user with the given role" do + auth_service = AuthService.new(mock_auth_adapter) + + auth_service.register("test@example.com", "password", "employer") + + user = User.find_by(uid: mock_uid) + expect(user).to be_present + expect(user.provider).to eq("mock") + expect(user.email).to eq("test@example.com") + expect(user.employer?).to eq(true) + end + end + + describe "#change_email" do + it "updates the user's email" do + auth_service = AuthService.new(mock_auth_adapter) + User.create!(uid: mock_uid, email: "test@example.com", provider: "mock") + + auth_service.change_email(mock_uid, "new@example.com") + + user = User.find_by(uid: mock_uid) + expect(user.email).to eq("new@example.com") + end + end + + describe "#initiate_auth" do + it "creates a new user if one does not exist" do + auth_service = AuthService.new(mock_auth_adapter) + + response = auth_service.initiate_auth("test@example.com", "password") + + user = User.find_by(uid: mock_uid) + expect(response[:user]).to eq(user) + expect(user.claimant?).to eq(true) + end + + it "updates the user's email if it has changed" do + auth_service = AuthService.new(mock_auth_adapter) + User.create!(uid: mock_uid, email: "oldie@example.com", provider: "mock") + + response = auth_service.initiate_auth("new@example.com", "password") + + user = User.find_by(uid: mock_uid) + expect(user.email).to eq("new@example.com") + end + end + + describe "#verify_account" do + it "returns empty struct on success" do + auth_service = AuthService.new(mock_auth_adapter) + + expect(auth_service.verify_account("test@example.com", "123456")).to eq({}) + end + end +end diff --git a/pfml/spec/spec_helper.rb b/pfml/spec/spec_helper.rb new file mode 100644 index 0000000..2922697 --- /dev/null +++ b/pfml/spec/spec_helper.rb @@ -0,0 +1,96 @@ +require 'pundit/matchers' + +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/pfml/spec/support/factory_bot.rb b/pfml/spec/support/factory_bot.rb new file mode 100644 index 0000000..c7890e4 --- /dev/null +++ b/pfml/spec/support/factory_bot.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end From 7abc001ff643c37cbdaf16a42f1a5df580a14469 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 9 Apr 2024 16:53:12 -0700 Subject: [PATCH 09/76] Add missing pieces --- app-rails/Makefile | 2 +- app-rails/local.env.example | 12 ++++---- pfml/app/assets/images/claimant.jpg | Bin 0 -> 61610 bytes pfml/app/assets/images/employer.jpg | Bin 0 -> 49946 bytes pfml/app/services/notification_service.rb | 33 ++++++++++++++++++++++ 5 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 pfml/app/assets/images/claimant.jpg create mode 100644 pfml/app/assets/images/employer.jpg create mode 100644 pfml/app/services/notification_service.rb diff --git a/app-rails/Makefile b/app-rails/Makefile index ad92f7a..7f20282 100644 --- a/app-rails/Makefile +++ b/app-rails/Makefile @@ -23,7 +23,7 @@ __check_defined = \ # Constants ################################################## -APP_NAME := app +APP_NAME := template_application_rails # Support other container tools like `finch` ifdef CONTAINER_CMD diff --git a/app-rails/local.env.example b/app-rails/local.env.example index c7749c2..172c13e 100644 --- a/app-rails/local.env.example +++ b/app-rails/local.env.example @@ -17,22 +17,22 @@ APP_PORT=3000 AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -BUCKET_NAME=pfml-starter-pfml-dev +AWS_DEFAULT_REGION= +BUCKET_NAME= ############################ # Auth ############################ COGNITO_CLIENT_SECRET= -COGNITO_USER_POOL_ID=us-east-1_o1btuFuDS -COGNITO_CLIENT_ID=35nrqeng2itarutlqh53tpsv3t +COGNITO_USER_POOL_ID= +COGNITO_CLIENT_ID= ############################ # Database ############################ DB_HOST=127.0.0.1 -DB_NAME=app -DB_USER=app +DB_NAME=template_application_rails +DB_USER=template_application_rails DB_PASSWORD=secret123 \ No newline at end of file diff --git a/pfml/app/assets/images/claimant.jpg b/pfml/app/assets/images/claimant.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0ee098964adf3dcdd721c17b615bcfc7d5a9379d GIT binary patch literal 61610 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 Hn)29XGS}7UNq1sRGlov;(QLDEt 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 literal 0 HcmV?d00001 diff --git a/pfml/app/assets/images/employer.jpg b/pfml/app/assets/images/employer.jpg new file mode 100644 index 0000000000000000000000000000000000000000..36a8a0d28054d3da16bd89537ce9808527fda298 GIT binary patch literal 49946 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 ^t$@@MkpUsHPO9= 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 Date: Wed, 10 Apr 2024 11:33:28 -0700 Subject: [PATCH 10/76] Add documentation --- README.md | 53 ++++++- app-rails/README.md | 119 +++++++++++++-- docs/template_application_rails/README.md | 15 ++ docs/template_application_rails/auth.md | 38 +++++ .../decisions}/README.md | 0 docs/template_application_rails/forms.md | 102 +++++++++++++ .../internationalization.md | 65 +++++++++ .../media/.keep} | 0 .../software-architecture.md | 84 +++++++++++ .../technical-foundation.md | 135 ++++++++++++++++++ 10 files changed, 597 insertions(+), 14 deletions(-) create mode 100644 docs/template_application_rails/README.md create mode 100644 docs/template_application_rails/auth.md rename docs/{app => template_application_rails/decisions}/README.md (100%) create mode 100644 docs/template_application_rails/forms.md create mode 100644 docs/template_application_rails/internationalization.md rename docs/{app/decisions/README.md => template_application_rails/media/.keep} (100%) create mode 100644 docs/template_application_rails/software-architecture.md create mode 100644 docs/template_application_rails/technical-foundation.md diff --git a/README.md b/README.md index 931e814..81697d4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,53 @@ # template-application-rails -Ruby on Rails with USWDS template, including CI/CD, for teams building web applications + +This is a template repository for a Ruby on Rails application. + +See [`navapbc/platform`](https://github.com/navapbc/platform) for other template repos. + +## Features + +- [U.S. Web Design System](https://designsystem.digital.gov/) for themeable styling and a set of common components +- Integration with AWS services, including + - Database integration with AWS RDS Postgresql using UUIDs + - Active Storage configuration with AWS S3 + - Action Mailer configuration with AWS SES + - Authentication with [devise](https://github.com/heartcombo/devise) and AWS Cognito +- Internationalization (i18n) +- Authorization using [pundit](https://github.com/varvet/pundit) +- Linting and code formatting using [rubocop](https://rubocop.org/) +- Testing using [rspec](https://rspec.info) + +## Repo structure + +```text +├── .github # GitHub workflows and repo templates +├── docs # Project docs and decision records +├── template_application_rails # Web application +``` + +## Installation + +To get started using the template application on your project: + +1. Run the [download and install script](./template-only-bin/download-and-install-template.sh) in your project's root directory. + + ```bash + curl https://raw.githubusercontent.com/navapbc/template-application-rails/main/template-only-bin/download-and-install-template.sh | bash -s + ``` + + This script will: + + 1. Clone the template repository + 2. Copy the template files into your project directory + 3. Remove any files specific to the template repository, like this README. +2. [Follow the steps in `template-application-rails/README.md`](./template-application-rails/README.md) to set up the application locally. +3. Optional, if using the Platform infra template: [Follow the steps in the `template-infra` README](https://github.com/navapbc/template-infra#installation) to set up the various pieces of your infrastructure. + +## Getting started + +Now that you're all set up, you're now ready to [get started](./template-application-rails/README.md). + +## Learn more about this template + +- [Dependency management](./template-only-docs/set-up-dependency-management.md) +- [Deployment](./template-only-docs/set-up-cd.md) diff --git a/app-rails/README.md b/app-rails/README.md index 7db80e4..91c96d9 100644 --- a/app-rails/README.md +++ b/app-rails/README.md @@ -1,24 +1,117 @@ -# README +# template-application-rails -This README would normally document whatever steps are necessary to get the -application up and running. +## Overview -Things you may want to cover: +This is a [Ruby on Rails](https://rubyonrails.org/) application. It includes: -* Ruby version +- [U.S. Web Design System](https://designsystem.digital.gov/) for themeable styling and a set of common components +- Integration with AWS services, including + - Database integration with AWS RDS Postgresql using UUIDs + - Active Storage configuration with AWS S3 + - Action Mailer configuration with AWS SES + - Authentication with [devise](https://github.com/heartcombo/devise) and AWS Cognito +- Internationalization (i18n) +- Authorization using [pundit](https://github.com/varvet/pundit) +- Linting and code formatting using [rubocop](https://rubocop.org/) +- Testing using [rspec](https://rspec.info) -* System dependencies +## 📂 Directory structure -* Configuration +As a Rails app, much of the directory structure is driven by Rails conventions. We've also included directories for common patterns, such as adapters, form objects and services. -* Database creation +**[Refer to the Software Architecture doc for more detail](../docs/template-application-rails/software-architecture.md)** -* Database initialization +Below are the primary directories to be aware of when working on the app: -* How to run the test suite +``` +├── app +│   ├── adapters # External services +│   │   └── *_adapter.rb +│   ├── controllers +│   ├── forms # Form objects +│   │   └── *_form.rb +│   ├── mailers +│   ├── models +│   │   └── concerns +│   ├── services # Shared cross-model business logic +│   │   └── *_service.rb +│   └── views +├── db +│   ├── migrate +│   └── schema.rb +├── config +│   ├── locales # i18n +│   └── routes.rb +├── spec # Tests +``` -* Services (job queues, cache servers, search engines, etc.) +## ðŸ’ŧ Getting started with local development -* Deployment instructions +### Prerequisites -* ... +- A container runtime (e.g. [Docker](https://www.docker.com/) or [Finch](https://github.com/runfinch/finch)) + - By default, `docker` is used. To change this, set the `CONTAINER_CMD` variable to `finch` (or whatever your container runtime is) in the shell. + +### ðŸ’ū Setup + +You can run the app within a container or natively. Each requires slightly different setup steps. + +#### Environment variables + +In either case, first generate a `.env` file: + +1. Run `make .env` to create a `.env` file based on shared template. +1. Variables marked with `` need to be manually set, and otherwise edit to your needs. + +#### Running in a container + +1. `make init-container` + +#### Running natively + +Prerequisites: + +- Ruby 3.3.0 +- [Node LTS](https://nodejs.org/en) +- (Optional but recommended): [rbenv](https://github.com/rbenv/rbenv) + +Steps: + +1. `make init-native` + +### 🛠ïļ Development + +#### Running the app + +Once you've completed the setup steps above, you can run the site natively or within a container runtime. + +To run within a container: + +1. `make start-container` +1. Then visit http://localhost:3100 + +To run natively: + +1. `make start-native` +1. Then visit http://localhost:3000 + +#### IDE tips + +
+VS Code + +##### Recommended extensions + +- [Ruby LSP](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp) +- [Simple ERB](https://marketplace.visualstudio.com/items?itemName=vortizhe.simple-ruby-erb), for tag autocomplete and snippets + +
+ +## 📇 Additional reading + +Beyond this README, you should also refer to the [`docs` directory](../docs/) for more detailed info. Some highlights: + +- [Technical foundation](../docs/template_application_rails/technical-foundation.md) +- [Software architecture](../docs/template_application_rails/software-architecture.md) +- [Authentication & Authorization](../docs/template_application_rails/auth.md) +- [Internationalization (i18n)](../docs/template_application_rails/internationalization.md) diff --git a/docs/template_application_rails/README.md b/docs/template_application_rails/README.md new file mode 100644 index 0000000..85161d7 --- /dev/null +++ b/docs/template_application_rails/README.md @@ -0,0 +1,15 @@ +# Documentation for template-application-rails + +## 📂 Directory structure + +```text +├── decisions # Architecture Decision Records +├── media # Diagrams, images +├── *.md # Documentation about the application +``` + +## Recommended reading + +- Start by reading the application's [main README](/template-application-rails/README.md) if you haven't already +- Next, review the [Technical Foundation](./technical-foundation.md) +- Continue reviewing the other documentation in this directory as desired \ No newline at end of file diff --git a/docs/template_application_rails/auth.md b/docs/template_application_rails/auth.md new file mode 100644 index 0000000..fa13a32 --- /dev/null +++ b/docs/template_application_rails/auth.md @@ -0,0 +1,38 @@ +# Authentication & Authorization + +## Claimant & Employer Authentication + +Authentication is the process of verifying the credentials of a user. We use AWS Cognito for authentication. + +- User credentials are stored in AWS Cognito +- Password policy is enforced by Cognito +- Custom pages are built for the AWS Cognito flows (login, signup, forgot password, etc.). We aren't using the hosted UI that Cognito provides since we need more control over the UI and content. +- [Devise](https://www.rubydoc.info/github/heartcombo/devise/main) and [Warden](https://www.rubydoc.info/github/hassox/warden) facilitate auth and session management + +## Authorization + +Authorization is the process of determining whether a user has access to a specific resource. We use [Pundit](https://github.com/varvet/pundit) for authorization. + +- User roles are defined in the `user_roles` table +- Policies (`app/policies`) are created for each model to define who can perform what actions +- Policies are used in controllers to authorize actions +- Policies are used in views to show/hide elements based on user permissions + +### Generating policies + +```sh +make new-authz-policy MODEL=Foo +``` + +### Testing policies + +[`pundit-matchers`](https://github.com/pundit-community/pundit-matchers) provides RSpec matchers for testing Pundit policies. Refer to existing policy spec files, or the spec file generated when creating a new policy, for examples. + +### Verifications + +We use a few `after_action` Pundit callbacks in the application controller to verify that our controllers are authorizing resources correctly. These aren't foolproof, but they can help catch some common mistakes: + +- If you forget to call `authorize` in a controller action, you'll see an exception like `AuthorizationNotPerformedError`. +- If you forget to call `policy_scope` in a controller action, you'll see an exception like `PolicyScopingNotPerformedError`. + +To opt out of these checks on actions that don't need them, you can add `skip_after_action :verify_authorized` or `skip_after_action :verify_policy_scoped` to your controller. Alternatively, you can add `skip_authorization` or `skip_policy_scope` to your controller action. diff --git a/docs/app/README.md b/docs/template_application_rails/decisions/README.md similarity index 100% rename from docs/app/README.md rename to docs/template_application_rails/decisions/README.md diff --git a/docs/template_application_rails/forms.md b/docs/template_application_rails/forms.md new file mode 100644 index 0000000..22032e7 --- /dev/null +++ b/docs/template_application_rails/forms.md @@ -0,0 +1,102 @@ +# Forms + +A custom `us_form_with` helper is provided to create forms. This adds the necessary U.S. Web Design System classes to the form and its elements, as well as simplifies the creation of labels, hint text, and inline error messages. + +The `us_form_with` helper is a drop-in replacement for the standard `form_with` helper. It accepts the same arguments and options as `form_with`, and exposes the same [field helpers](https://guides.rubyonrails.org/form_helpers.html). + +Example usage: + +```erb +<%= us_form_with model: @claim do |f| %> + <%= f.text_field :name, { hint: t(".name.hint") } %> + + <%= f.fieldset t("claim_types.legend") do %> + <%= f.radio_button :claim_type, "medical" %> + <%= f.radio_button :claim_type, "family" %> + <% end %> + + <%= f.submit %> +<% end %> +``` + +## Labels + +When using the `us_form_with` helper, you don't need to use a separate `label` helper. Instead, you can pass a `label` option to the field helper: + +```erb +<%= f.text_field :name, { label: "Full name" } %> +``` + +## Hint text + +You can add hint text to a field by passing a `hint` option to the field helper: + +```erb +<%= f.text_field :name, { hint: "Enter your full name" } %> +``` + +To include hint text within a fieldset, use the `hint` helper: + +```erb +<%= f.fieldset "Notification preferences" do %> + <%= f.hint "Select the ways you'd like to be notified" %> +``` + +## Fieldsets + +Use the custom `fieldset` helper to create a fieldset with a legend, rather than the standard `field_set_tag` helper. + +```erb +<%= f.fieldset "Notification preferences" do %> + <%= f.check_box :email %> + <%= f.check_box :sms %> +<% end %> +``` + +Use `field_error` to display an error message for a radio group: + +```erb +<%= f.fieldset "Plan" do %> + <%= f.field_error :plan %> + <%= f.radio_button :plan, "health" %> + <%= f.radio_button :plan, "medical" %> +<% end %> +``` + +A `human_name` helper is provided to format a field name for display to the user. This is primarily useful for fieldset legends – other fields already utilize this behind the scenes. + +```erb +<%= f.fieldset f.human_name(:notification_preferences) do %> + <%= f.check_box :email %> + <%= f.check_box :sms %> +<% end %> +``` + +## Yes/No fields + +Use the custom `yes_no` helper to create a pair of radio buttons for a boolean field: + +```erb +<%= f.yes_no :has_previous_leave %> +``` + +To apply custom labels or hint text to the radio buttons, pass in `yes_options` and `no_options`: + +```erb +<%= f.yes_no :has_previous_leave, { + yes_options: { label: "Yes, I've taken leave before" }, + no_options: { label: "No, I haven't taken leave before" } +} %> +``` + +## Text inputs + +Control the width of a text input by passing a `width` option to the field helper, with a value corresponding to the `width` values accepted by [the `usa-input--[width]` class](https://designsystem.digital.gov/components/text-input/#using-the-text-input-component-2). + +```erb +<%= f.text_field :name, { width: "md" } %> +``` + +## Testing + +When running the app locally, a test form is available at `/dev/sandbox` diff --git a/docs/template_application_rails/internationalization.md b/docs/template_application_rails/internationalization.md new file mode 100644 index 0000000..00a9af5 --- /dev/null +++ b/docs/template_application_rails/internationalization.md @@ -0,0 +1,65 @@ +# Internationalization + +We use Rails' built-in internationalization (i18n) to support multiple languages, [read the general documentation here](https://guides.rubyonrails.org/i18n.html). + +To create new locale files for a model and/or its views, you can run: + +```sh +make locale MODEL=MyModel +``` + +## Organization of locale files + +Placing translations for all parts of an application in one file per locale can become hard to manage. We've chosen to organize our translations into multiple YAML files, using the following hierarchy + +- `defaults` - fallback errors, date, and time formats +- `models` - model and attribute names +- `views` - strings specifically rendered in views, partials, or layouts + +> [!CAUTION] +> Be aware that YAML interprets the following case-insensitive strings as booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings must be quoted to be interpreted as strings. For example: `"yes": yup` and `enabled: "ON"` + +## Routes and links + +The active locale is based on the URL. For example, to use the Spanish locale, visit: `/es-US`. + +When adding a new route, ensure it's nested within the `localized` block so that it can be viewed at the appropriate locale. + +To ensure links preserve the locale, it's important for them to include the locale param, which happens automatically when using `link_to` or `url_for` helpers: + +```erb +<%= link_to "Apply", controller: "claims", action: "new" %> +``` + +## Rendering content + +To render content, use `I18n.t`: + +```ruby +I18n.t("hello") +``` + +In views, this is aliased to just `t`: + +```erb +<%= t("hello") %> +``` + +### Dates + +To format dates, use the custom `local_time` helper: + +```erb +<%= local_time(foo.created_at) %> +``` + +To include the time, use the `:format` option: + +```erb +<%= local_time(foo.created_at, format: :long) %> +``` + +## Adding a new language + +1. Add new YAML file(s) in `config/locales` +1. Update `available_locales` in `config/application.rb` to include the new locale diff --git a/docs/app/decisions/README.md b/docs/template_application_rails/media/.keep similarity index 100% rename from docs/app/decisions/README.md rename to docs/template_application_rails/media/.keep diff --git a/docs/template_application_rails/software-architecture.md b/docs/template_application_rails/software-architecture.md new file mode 100644 index 0000000..591c09d --- /dev/null +++ b/docs/template_application_rails/software-architecture.md @@ -0,0 +1,84 @@ +# Software architecture + +## Rails + +Rails follows the [Model-View-Controller (MVC)](https://guides.rubyonrails.org/getting_started.html#mvc-and-you) pattern. + +### Controllers & routing + +Controllers contain actions which handle routing requests. Controllers should be relatively "dumb" and should not contain any business logic. + +When handling CRUD operations, controllers are responsible for initiating persistence operations, such as quering the database and instantiating objects or creating new records. Controllers display data for requests by rendering views. + +### Models & inheritance + +Models are ruby classes and can only have one parent. + +Domain modeling is expressed in rails through models. Models in rails are powerful because of ActiveRecord and [associations](https://guides.rubyonrails.org/association_basics.html). All database relationships should be expressed through the ActiveRecord. + +Model inheritance is very powerful, but slightly complicated. [Single table inheritance](https://guides.rubyonrails.org/association_basics.html#single-table-inheritance-sti) should only be used if the subclasses have largely the same attributes/database fields. STI creates one table that is used for all subclasses; this means that all fields for all the subclasses are appended to the same table. + +If the subclasses have many distinct attributes, use [delegated types](https://guides.rubyonrails.org/association_basics.html#delegated-types) instead. Delegated types creates a single shared table for shared attributes, but maintains separate tables for each subclass's distinct attributes. + +### Validations & callbacks + +ActiveRecord provides an easy way to handle model [validations](https://guides.rubyonrails.org/active_record_validations.html) with many built-in validators. Any validation logic that should be used in more than one model (e.g. validating a tax id) should be moved into a custom validator. + +[Callbacks](https://guides.rubyonrails.org/active_record_callbacks.html) in rails are powerful ways to hook into the object lifecycle, allowing events to trigger custom actions. Callbacks should primarily be used for persistence logic: logic that is related to persisting information to the database. Business logic should generally be expressed as methods that can be optionally invoked. Drawing the line between persistence logic and business logic can be challenging. + +### Concerns + +Concerns are ruby modules and single class can have multiple modules can be mixed into it. Rails supports concerns for models and controllers. Any reusable code that can be DRY'd should be moved into a concern. + +Special care should be taken with model concerns. Model concerns make assumptions about what attributes the model that includes it will have defined. This leans heavily into ruby's preference for duck typing. This allows for a lot of flexibility but defensive programming should be employed so that guardrails prevent unwanted behavior. + +## Additional patterns + +### Services + +Services encapsulate business logic. They are typically used by controllers to perform actions and return data. Services should receive external dependencies as arguments (AKA dependency injection) to make testing easier. Services should be thin wrappers and should not contain explicit knowledge about models. + +### Adapters + +Adapters encapsulate interactions with external services. A mock adapter can be used in tests to avoid making real requests. + +### Form objects + +Form objects are responsible for validation and data manipulation that needs to happen before data is persisted (i.e combining fields, formatting, etc). + +These are for forms not directly tied to a single ActiveRecord model or backed by a database table. + +Benefits of form objects are: + +- Separation of concerns: validation logic is separated from the controller +- Testability: validation logic can be unit tested without needing to test the controller +- Consistency: use the same ActiveModel validations as ActiveRecord models +- Reusability: can be reused across multiple controllers + +### Tying it all together + +Controllers handle requests, using form objects to validate and manipulate data. + +Controllers initialize adapters, which are passed into services, which use the adapters to interact with external services. This allows us to swap adapters for testing, to avoid making real requests. + +```mermaid +graph TD + Ctrl[Controller] + Service[Service] + Form[Form object] + Adpt[Adapter] + + Jobs -->|uses| Service + Ctrl -->|uses| Service + Ctrl -->|uses| Form + Ctrl -->|renders| View + + Form -->|uses| AM[ActiveModel] + + Service -->|uses| Adpt + Service -->|uses| AR[ActiveRecord] + AR -->|callbacks| AR + + Adpt -->|calls| ExtSvc[External Service] + AR -->|persists| DB[Database] +``` diff --git a/docs/template_application_rails/technical-foundation.md b/docs/template_application_rails/technical-foundation.md new file mode 100644 index 0000000..7fb0353 --- /dev/null +++ b/docs/template_application_rails/technical-foundation.md @@ -0,0 +1,135 @@ +# Technical foundation + +## 🧑‍ðŸŽĻ Frontend + +### 🇚ðŸ‡ļ USWDS + +The frontend utilizes the [U.S. Web Design System (USWDS)](https://designsystem.digital.gov/) for styling and common components. + +To reference USWDS assets, use Rails asset helpers like: + +```erb +<%= image_tag "@uswds/uswds/dist/img/usa-icons/close.svg", alt: "Close" %> + +Close +``` + +### 🌎 Internationalization (i18n) + +The app uses [Rails' built-in i18n support](https://guides.rubyonrails.org/i18n.html). Locale strings are located in [`config/locales`](/pfml/config/locales). + +Refer to the [Internationalization doc](./internationalization.md) for more details and conventions. + +## ⚙ïļ Backend + +### ðŸ’― Database + +Postgresql is used for the database. + +#### Commands + +- Run migrations: `make db-migrate` +- Seed the database: `make db-seed` +- Reset the database: `make db-reset` + - This will drop the database, recreate the schema based on `db/schema.rb` (not the migrations!), and seed the database +- Access the database console: `make db-console` + +#### UUIDs + +We have are using [UUIDs for primary keys](https://guides.rubyonrails.org/active_record_postgresql.html#uuid-primary-keys) instead of autoincrementing integers. This has a few implications to beware of: + +- ⚠ïļ Using ActiveRecord functions like `Foo.first` and `Foo.last` have unreliable results +- Generating new models or scaffolds requires passing the `--primary-key-type=uuid` flag. For instance, `make rails-generate GENERATE_COMMAND="model Foo --primary-key-type=uuid"` +- `pgcrypto` is a required dependency + +#### Enums + +⚠ïļ Important! Enum order cannot be changed. +From https://api.rubyonrails.org/v7.1.3.2/classes/ActiveRecord/Enum.html: + +> ... once a value is added to the enum array, its position in the array must be +> maintained, and new values should only be added to the end of the array. To remove +> unused values, the explicit hash syntax should be used. + +- Use explicit hashes that map the enum symbol to an integer, instead of implicit arrays. For example: `{ approved: 0, denied: 1}` instead of `[:approved, :denied]`. + +### ðŸ“Ŧ Notifications + +The app uses [Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html) for sending email notifications. During local development, it uses `letter_opener` to open emails in the browser instead of sending them. + +To preview email views in the browser, visit: `/rails/mailers` + +To test AWS SES email sending locally: + +1. Set the `AWS_*` env var in your `.env` file +1. Set the `SES_EMAIL` env var to a verified sending identity +1. Restart the server + +### 🎭 Authentication + +The app provides a service and configuration for authentication with AWS Cognito. + +Refer to the [Auth doc](./auth.md) for more details and conventions. + +## 🔄 Continuous integration + +### 🧊 Unit testing + +[RSpec](https://rspec.info/) is used for testing. [Capybara](https://www.rubydoc.info/gems/capybara/Capybara/RSpecMatchers) matchers are available. + +Run tests with: + +```sh +make test +``` + +Pass additional arguments to `rspec` with `args`: + +```sh +make test args="spec/path/to/specific_test.rb" +``` + +### ðŸ§đ Linting + +[Rubocop](https://rubocop.org/) is used for linting. + +Run linting with auto-fixing with: + +```sh +make lint +``` + +## â„đïļ Developer norms, tooling, and tips + +### ðŸĪ– Norms + +Use the rails generator when creating new models, migrations, etc. It does most of the heavy lifting for you. + +To create a full scaffold of controller, views, model, database migration: + +```sh +make rails-generate GENERATE_COMMAND="scaffold Foo --primary-key-type=uuid" +``` + +To create a database migration: + +```sh +make rails-generate GENERATE_COMMAND="migration AddColumnToTable" +``` + +To create a model: + +```sh +make rails-generate GENERATE_COMMAND="model Foo --primary-key-type=uuid" +``` + +### 🐛 Debugging + +Rails has some useful [built-in debugging tools](https://guides.rubyonrails.org/debugging_rails_applications.html). + +- Start the rails console: `make rails-console` +- Run a console in the browser: + - Add a `console` line and an interactive console, similar to the rails console, will appear in the bottom half of your browser window +- Run the debugger: + - Add a `debugger` line and the rails server will pause and start the debugger + - Note: this is not yet configured for this application when doing local development in a container From 03030e7b47b5cf1d3aca67e0dda68ca294d442f5 Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 10 Apr 2024 14:09:40 -0700 Subject: [PATCH 11/76] Rename paths to use kebab case instead of snake case --- README.md | 4 ++-- app-rails/README.md | 22 +++++++++---------- .../README.md | 0 .../auth.md | 0 .../decisions/README.md | 0 .../forms.md | 0 .../internationalization.md | 0 .../media/.keep | 0 .../software-architecture.md | 0 .../technical-foundation.md | 0 10 files changed, 12 insertions(+), 14 deletions(-) rename docs/{template_application_rails => template-application-rails}/README.md (100%) rename docs/{template_application_rails => template-application-rails}/auth.md (100%) rename docs/{template_application_rails => template-application-rails}/decisions/README.md (100%) rename docs/{template_application_rails => template-application-rails}/forms.md (100%) rename docs/{template_application_rails => template-application-rails}/internationalization.md (100%) rename docs/{template_application_rails => template-application-rails}/media/.keep (100%) rename docs/{template_application_rails => template-application-rails}/software-architecture.md (100%) rename docs/{template_application_rails => template-application-rails}/technical-foundation.md (100%) diff --git a/README.md b/README.md index 81697d4..d77542f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# template-application-rails +# Template Ruby on Rails application This is a template repository for a Ruby on Rails application. @@ -22,7 +22,7 @@ See [`navapbc/platform`](https://github.com/navapbc/platform) for other template ```text ├── .github # GitHub workflows and repo templates ├── docs # Project docs and decision records -├── template_application_rails # Web application +├── template-application-rails # Web application ``` ## Installation diff --git a/app-rails/README.md b/app-rails/README.md index 91c96d9..db80d42 100644 --- a/app-rails/README.md +++ b/app-rails/README.md @@ -1,5 +1,3 @@ -# template-application-rails - ## Overview This is a [Ruby on Rails](https://rubyonrails.org/) application. It includes: @@ -25,24 +23,24 @@ Below are the primary directories to be aware of when working on the app: ``` ├── app -│   ├── adapters # External services +│   ├── adapters # External services │   │   └── *_adapter.rb │   ├── controllers -│   ├── forms # Form objects +│   ├── forms # Form objects │   │   └── *_form.rb │   ├── mailers │   ├── models │   │   └── concerns -│   ├── services # Shared cross-model business logic +│   ├── services # Shared cross-model business logic │   │   └── *_service.rb │   └── views ├── db │   ├── migrate │   └── schema.rb ├── config -│   ├── locales # i18n +│   ├── locales # i18n │   └── routes.rb -├── spec # Tests +├── spec # Tests ``` ## ðŸ’ŧ Getting started with local development @@ -109,9 +107,9 @@ To run natively: ## 📇 Additional reading -Beyond this README, you should also refer to the [`docs` directory](../docs/) for more detailed info. Some highlights: +Beyond this README, you should also refer to the [`docs` directory](../docs/template-application-rails) for more detailed info. Some highlights: -- [Technical foundation](../docs/template_application_rails/technical-foundation.md) -- [Software architecture](../docs/template_application_rails/software-architecture.md) -- [Authentication & Authorization](../docs/template_application_rails/auth.md) -- [Internationalization (i18n)](../docs/template_application_rails/internationalization.md) +- [Technical foundation](../docs/template-application-rails/technical-foundation.md) +- [Software architecture](../docs/template-application-rails/software-architecture.md) +- [Authentication & Authorization](../docs/template-application-rails/auth.md) +- [Internationalization (i18n)](../docs/template-application-rails/internationalization.md) diff --git a/docs/template_application_rails/README.md b/docs/template-application-rails/README.md similarity index 100% rename from docs/template_application_rails/README.md rename to docs/template-application-rails/README.md diff --git a/docs/template_application_rails/auth.md b/docs/template-application-rails/auth.md similarity index 100% rename from docs/template_application_rails/auth.md rename to docs/template-application-rails/auth.md diff --git a/docs/template_application_rails/decisions/README.md b/docs/template-application-rails/decisions/README.md similarity index 100% rename from docs/template_application_rails/decisions/README.md rename to docs/template-application-rails/decisions/README.md diff --git a/docs/template_application_rails/forms.md b/docs/template-application-rails/forms.md similarity index 100% rename from docs/template_application_rails/forms.md rename to docs/template-application-rails/forms.md diff --git a/docs/template_application_rails/internationalization.md b/docs/template-application-rails/internationalization.md similarity index 100% rename from docs/template_application_rails/internationalization.md rename to docs/template-application-rails/internationalization.md diff --git a/docs/template_application_rails/media/.keep b/docs/template-application-rails/media/.keep similarity index 100% rename from docs/template_application_rails/media/.keep rename to docs/template-application-rails/media/.keep diff --git a/docs/template_application_rails/software-architecture.md b/docs/template-application-rails/software-architecture.md similarity index 100% rename from docs/template_application_rails/software-architecture.md rename to docs/template-application-rails/software-architecture.md diff --git a/docs/template_application_rails/technical-foundation.md b/docs/template-application-rails/technical-foundation.md similarity index 100% rename from docs/template_application_rails/technical-foundation.md rename to docs/template-application-rails/technical-foundation.md From 4c9a0f5fa1b1f37e10e75b556fad7b9648ca4fbb Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 10 Apr 2024 14:12:30 -0700 Subject: [PATCH 12/76] Document template-only- dirs --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d77542f..2bd27fb 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ See [`navapbc/platform`](https://github.com/navapbc/platform) for other template ├── .github # GitHub workflows and repo templates ├── docs # Project docs and decision records ├── template-application-rails # Web application +├── template-only-bin # Scripts for managing this template; not copied into your project +├── template-only-docs # Documentation for this template; not copied into your project ``` ## Installation From 4d0bfb110e53994cd3159ff0e948fd8e69bae653 Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 10 Apr 2024 14:33:56 -0700 Subject: [PATCH 13/76] Setup database --- ...te_active_storage_tables.active_storage.rb | 57 ++++++++++++++++ .../db/migrate/20240410212926_create_users.rb | 13 ++++ .../20240410213056_create_user_roles.rb | 10 +++ app-rails/db/schema.rb | 66 +++++++++++++++++++ docker-compose.yml | 47 +++++++++++++ init-postgres.sql | 4 ++ 6 files changed, 197 insertions(+) create mode 100644 app-rails/db/migrate/20240410212818_create_active_storage_tables.active_storage.rb create mode 100644 app-rails/db/migrate/20240410212926_create_users.rb create mode 100644 app-rails/db/migrate/20240410213056_create_user_roles.rb create mode 100644 app-rails/db/schema.rb create mode 100644 init-postgres.sql diff --git a/app-rails/db/migrate/20240410212818_create_active_storage_tables.active_storage.rb b/app-rails/db/migrate/20240410212818_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000..e4706aa --- /dev/null +++ b/app-rails/db/migrate/20240410212818_create_active_storage_tables.active_storage.rb @@ -0,0 +1,57 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[7.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/app-rails/db/migrate/20240410212926_create_users.rb b/app-rails/db/migrate/20240410212926_create_users.rb new file mode 100644 index 0000000..5c52bfb --- /dev/null +++ b/app-rails/db/migrate/20240410212926_create_users.rb @@ -0,0 +1,13 @@ +class CreateUsers < ActiveRecord::Migration[7.1] + def change + create_table :users, id: :uuid do |t| + t.string "uid", null: false + t.string "provider", null: false + t.string "email", default: "", null: false + t.integer "mfa_preference" + t.index ["uid"], name: "index_users_on_uid", unique: true + + t.timestamps + end + end +end diff --git a/app-rails/db/migrate/20240410213056_create_user_roles.rb b/app-rails/db/migrate/20240410213056_create_user_roles.rb new file mode 100644 index 0000000..d5b5535 --- /dev/null +++ b/app-rails/db/migrate/20240410213056_create_user_roles.rb @@ -0,0 +1,10 @@ +class CreateUserRoles < ActiveRecord::Migration[7.1] + def change + create_table :user_roles, id: :uuid do |t| + t.references "user", type: :uuid, null: false, foreign_key: true + t.integer "role", null: false + + t.timestamps + end + end +end diff --git a/app-rails/db/schema.rb b/app-rails/db/schema.rb new file mode 100644 index 0000000..709bdc0 --- /dev/null +++ b/app-rails/db/schema.rb @@ -0,0 +1,66 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.1].define(version: 2024_04_10_213056) do + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "active_storage_attachments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.uuid "record_id", null: false + t.uuid "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + + create_table "user_roles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "user_id", null: false + t.integer "role", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_user_roles_on_user_id" + end + + create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "uid", null: false + t.string "provider", null: false + t.string "email", default: "", null: false + t.integer "mfa_preference" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["uid"], name: "index_users_on_uid", unique: true + end + + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "user_roles", "users" +end diff --git a/docker-compose.yml b/docker-compose.yml index e69de29..e303515 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +services: + database: + image: postgres:14-alpine + command: postgres -c "log_lock_waits=on" -N 1000 -c "fsync=off" + environment: + POSTGRES_PASSWORD: secret123 + POSTGRES_USER: template_application_rails + healthcheck: + test: "pg_isready --username=template_application_rails" + timeout: 10s + retries: 20 + ports: + - "5432:5432" + volumes: + - database_data:/var/lib/postgresql/data + - ./init-postgres.sql:/docker-entrypoint-initdb.d/init-postgres.sql + + # Rails app + # Configured for "development" RAILS_ENV + template-application-rails: + build: + context: ./app + target: dev + depends_on: + database: + condition: service_healthy + env_file: ./app/.env + environment: + - DB_HOST=database + - RAILS_BINDING=0.0.0.0 + - RAILS_ENV=development + ports: + - 3100:3000 + volumes: + - ./app:/rails + # Use named volumes for directories that the container should use the guest + # machine's dir instead of the host machine's dir, which may be divergent. + # This is especially true for any dependency or temp directories. + - app_rails_nodemodules:/rails/node_modules + - app_rails_tmp:/rails/tmp + - app_rails_storage:/rails/storage + +volumes: + database_data: + app_rails_nodemodules: + app_rails_tmp: + app_rails_storage: diff --git a/init-postgres.sql b/init-postgres.sql new file mode 100644 index 0000000..8f92056 --- /dev/null +++ b/init-postgres.sql @@ -0,0 +1,4 @@ +-- This is run when you start the Docker postgres container. +-- Create separate databases for applications. + +CREATE DATABASE template_application_rails; \ No newline at end of file From aa9525cbfead67483cb35df5eec9a6adc2760003 Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 10 Apr 2024 14:34:42 -0700 Subject: [PATCH 14/76] Fix referenced name --- app-rails/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-rails/Makefile b/app-rails/Makefile index 7f20282..d92a229 100644 --- a/app-rails/Makefile +++ b/app-rails/Makefile @@ -23,7 +23,7 @@ __check_defined = \ # Constants ################################################## -APP_NAME := template_application_rails +APP_NAME := template-application-rails # Support other container tools like `finch` ifdef CONTAINER_CMD From c9ad645bc8634179657f01fcfedcf5aa2ac5cb23 Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 10 Apr 2024 14:35:11 -0700 Subject: [PATCH 15/76] *Optimize docker layer caching --- app-rails/Dockerfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app-rails/Dockerfile b/app-rails/Dockerfile index dccbebd..7623d77 100644 --- a/app-rails/Dockerfile +++ b/app-rails/Dockerfile @@ -30,8 +30,8 @@ FROM base as build RUN apt-get update -qq && \ apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config npm -# Copy application code -COPY . . +# Install npm packages +COPY package.json package-lock.json ./ # Install npm packages RUN npm install @@ -55,6 +55,9 @@ RUN bundle config set --local without production && \ bundle install && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git +# Copy application code +COPY . . + CMD ["./bin/dev"] From 43be85f093c52316db829df22d11f467436bd190 Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 10 Apr 2024 14:35:24 -0700 Subject: [PATCH 16/76] *Fix waiting for database --- app-rails/bin/wait-for-local-postgres.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-rails/bin/wait-for-local-postgres.sh b/app-rails/bin/wait-for-local-postgres.sh index 20cbbd3..049073f 100755 --- a/app-rails/bin/wait-for-local-postgres.sh +++ b/app-rails/bin/wait-for-local-postgres.sh @@ -43,7 +43,7 @@ until pg_isready -h localhost -p $DB_PORT -d local-postgres-db -q; do wait_time=$(($wait_time + 3)) if [ $wait_time -gt $MAX_WAIT_TIME ]; then echo -e "${RED}ERROR: Database appears to not be starting up, running \"${DOCKER_CMD} logs main-db\" to troubleshoot${NO_COLOR}" - ${DOCKER_CMD} logs main-db + ${DOCKER_CMD} logs database exit 1 fi done From 6804d5ae03a3cf7e6418493b037b4d40e0ae9bec Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 10 Apr 2024 18:16:38 -0700 Subject: [PATCH 17/76] Namespace the helper because it was moved --- pfml/app/helpers/users/mfa_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfml/app/helpers/users/mfa_helper.rb b/pfml/app/helpers/users/mfa_helper.rb index 7b291c7..c3cc5c0 100644 --- a/pfml/app/helpers/users/mfa_helper.rb +++ b/pfml/app/helpers/users/mfa_helper.rb @@ -1,4 +1,4 @@ -module MfaHelper +module Users::MfaHelper # Generate a QR code for setting up the TOTP device def totp_qr_code(secret, email) RQRCode::QRCode.new(totp_qr_code_uri(secret, email)).as_svg( From 060f3935249545edfe5570ec42dd3bb23e3f6ae9 Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 10 Apr 2024 18:16:57 -0700 Subject: [PATCH 18/76] Fix sandbox --- app-rails/app/controllers/dev_controller.rb | 33 +++++++++++---------- pfml/app/views/application/sandbox.html.erb | 24 ++++++++------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/app-rails/app/controllers/dev_controller.rb b/app-rails/app/controllers/dev_controller.rb index 6a12abe..b28eeb3 100644 --- a/app-rails/app/controllers/dev_controller.rb +++ b/app-rails/app/controllers/dev_controller.rb @@ -3,27 +3,30 @@ class DevController < ApplicationController # Frontend sandbox for testing purposes during local development def sandbox + skip_authorization end + # Note: This currently doesn't work because the RequestForInformationMailer requires a clarifiable object. + # If we want to exercise this functionality, we should probably create a test Mailer. # Trigger an email to the email param - def send_email - email = params[:email] + # def send_email + # email = params[:email] - if email.present? - RequestForInformationMailer.with(email_address: email, name: "Anton Weis").task_opened.deliver_now - flash[:notice] = "Email sent to #{email}" - else - flash[:error] = "No email provided" - end - redirect_to dev_sandbox_path - end + # if email.present? + # RequestForInformationMailer.with(email_address: email).task_opened.deliver_now + # flash[:notice] = "Email sent to #{email}" + # else + # flash[:error] = "No email provided" + # end + # redirect_to dev_sandbox_path + # end private - def check_env - unless Rails.env.development? - redirect_to root_path - nil - end + def check_env + unless Rails.env.development? + redirect_to root_path + nil end + end end diff --git a/pfml/app/views/application/sandbox.html.erb b/pfml/app/views/application/sandbox.html.erb index a96e774..d4b5cc8 100644 --- a/pfml/app/views/application/sandbox.html.erb +++ b/pfml/app/views/application/sandbox.html.erb @@ -2,18 +2,9 @@

Sandbox

-

Notifications

- -<%= us_form_with url: dev_send_email_path do |f| %> - <%= f.email_field :email %> - <%= f.submit 'Send' %> -<% end %> - -
-

us_form_with

-<%= us_form_with url: authenticate_user_path do |f| %> +<%= us_form_with url: user_session_en_path do |f| %> <%= f.text_field :username, { label: "Name", hint: 'As shown on your legal ID'} %> <%= f.email_field :email, { class: 'usa-input--md' } %> <%= f.password_field :password, { hint: "Test hint" } %> @@ -41,4 +32,15 @@ <%= f.file_field :picture %> <%= f.submit 'Save and continue' %> -<% end %> \ No newline at end of file +<% end %> + +

Notifications

+ +

+ This currently does not work. +

+ +<%= us_form_with url: dev_send_email_path do |f| %> + <%= f.email_field :email %> + <%= f.submit 'Send' %> +<% end %> From 42149c545f1a8e5f27afd9d57615dfd3ce2a9790 Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 11:49:35 -0700 Subject: [PATCH 19/76] Remove non-working sandbox notifications for now --- app-rails/app/controllers/dev_controller.rb | 15 --------------- pfml/app/views/application/sandbox.html.erb | 11 ----------- 2 files changed, 26 deletions(-) diff --git a/app-rails/app/controllers/dev_controller.rb b/app-rails/app/controllers/dev_controller.rb index b28eeb3..39b2aad 100644 --- a/app-rails/app/controllers/dev_controller.rb +++ b/app-rails/app/controllers/dev_controller.rb @@ -6,21 +6,6 @@ def sandbox skip_authorization end - # Note: This currently doesn't work because the RequestForInformationMailer requires a clarifiable object. - # If we want to exercise this functionality, we should probably create a test Mailer. - # Trigger an email to the email param - # def send_email - # email = params[:email] - - # if email.present? - # RequestForInformationMailer.with(email_address: email).task_opened.deliver_now - # flash[:notice] = "Email sent to #{email}" - # else - # flash[:error] = "No email provided" - # end - # redirect_to dev_sandbox_path - # end - private def check_env diff --git a/pfml/app/views/application/sandbox.html.erb b/pfml/app/views/application/sandbox.html.erb index d4b5cc8..a4ce636 100644 --- a/pfml/app/views/application/sandbox.html.erb +++ b/pfml/app/views/application/sandbox.html.erb @@ -33,14 +33,3 @@ <%= f.submit 'Save and continue' %> <% end %> - -

Notifications

- -

- This currently does not work. -

- -<%= us_form_with url: dev_send_email_path do |f| %> - <%= f.email_field :email %> - <%= f.submit 'Send' %> -<% end %> From cdb7aeea75ac3009b76e3c1dc397634eea0ad97f Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 13:40:14 -0700 Subject: [PATCH 20/76] Remove unused files --- .../app/assets/stylesheets/application.css | 15 ------- .../controllers/hello_controller.js | 7 ---- app-rails/config/locales/en.yml | 41 ------------------- 3 files changed, 63 deletions(-) delete mode 100644 app-rails/app/assets/stylesheets/application.css delete mode 100644 app-rails/app/javascript/controllers/hello_controller.js delete mode 100644 app-rails/config/locales/en.yml diff --git a/app-rails/app/assets/stylesheets/application.css b/app-rails/app/assets/stylesheets/application.css deleted file mode 100644 index 288b9ab..0000000 --- a/app-rails/app/assets/stylesheets/application.css +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's - * vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - */ diff --git a/app-rails/app/javascript/controllers/hello_controller.js b/app-rails/app/javascript/controllers/hello_controller.js deleted file mode 100644 index 5975c07..0000000 --- a/app-rails/app/javascript/controllers/hello_controller.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - connect() { - this.element.textContent = "Hello World!" - } -} diff --git a/app-rails/config/locales/en.yml b/app-rails/config/locales/en.yml deleted file mode 100644 index 80db087..0000000 --- a/app-rails/config/locales/en.yml +++ /dev/null @@ -1,41 +0,0 @@ -# See: https://guides.rubyonrails.org/i18n.html. -# -# Be aware that YAML interprets the following case-insensitive strings as -# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings -# must be quoted to be interpreted as strings. For example: -# "yes": yup -# enabled: "ON" - -en: - header: - account: My account - language_toggle: English - logout: "Log out" - title: "Paid Family & Medical Leave" - close: "Close" - flash: - error_heading: - zero: "An unexpected error occurred" - one: "An error occurred" - other: "%{count} errors occurred" - error_fallback: "Sorry, we encountered an unexpected error. If this continues to happen, you may call the Paid Family Leave Contact Center." - reload_page: "Reload page" - breadcrumbs: - home: "Home" - buttons: - submit: "Submit" - requestable_step_indicator: - submitted: "Submitted" - in_review: "In review" - determination_made: "Determination made" - requestable_index: - existing_requests: - heading: "Existing requests" - col_created_at: "Date submitted" - col_status: "Status" - col_actions: "Actions" - action_view_rfi: "Respond to RFI" - action_none: "You have no pending actions at this time." - tables: - column: "Column" - description: "Description" From 8881fc4b499b0e2a20112d354f8acc258749dacc Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 13:53:25 -0700 Subject: [PATCH 21/76] Rename everything to app-rails/app_rails --- README.md | 6 +++--- app-rails/Makefile | 2 +- app-rails/README.md | 12 ++++++------ app-rails/config/cable.yml | 2 +- app-rails/config/database.yml | 4 ++-- app-rails/config/environments/production.rb | 2 +- app-rails/local.env.example | 4 ++-- docker-compose.yml | 10 +++++----- .../README.md | 4 ++-- .../auth.md | 0 .../decisions/README.md | 0 .../forms.md | 0 .../internationalization.md | 0 .../media/.keep | 0 .../software-architecture.md | 0 .../technical-foundation.md | 0 init-postgres.sql | 2 +- 17 files changed, 24 insertions(+), 24 deletions(-) rename docs/{template-application-rails => app-rails}/README.md (69%) rename docs/{template-application-rails => app-rails}/auth.md (100%) rename docs/{template-application-rails => app-rails}/decisions/README.md (100%) rename docs/{template-application-rails => app-rails}/forms.md (100%) rename docs/{template-application-rails => app-rails}/internationalization.md (100%) rename docs/{template-application-rails => app-rails}/media/.keep (100%) rename docs/{template-application-rails => app-rails}/software-architecture.md (100%) rename docs/{template-application-rails => app-rails}/technical-foundation.md (100%) diff --git a/README.md b/README.md index 2bd27fb..1ca77a1 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ See [`navapbc/platform`](https://github.com/navapbc/platform) for other template ```text ├── .github # GitHub workflows and repo templates ├── docs # Project docs and decision records -├── template-application-rails # Web application +├── app-rails # Web application ├── template-only-bin # Scripts for managing this template; not copied into your project ├── template-only-docs # Documentation for this template; not copied into your project ``` @@ -42,12 +42,12 @@ To get started using the template application on your project: 1. Clone the template repository 2. Copy the template files into your project directory 3. Remove any files specific to the template repository, like this README. -2. [Follow the steps in `template-application-rails/README.md`](./template-application-rails/README.md) to set up the application locally. +2. [Follow the steps in `app-rails/README.md`](./app-rails/README.md) to set up the application locally. 3. Optional, if using the Platform infra template: [Follow the steps in the `template-infra` README](https://github.com/navapbc/template-infra#installation) to set up the various pieces of your infrastructure. ## Getting started -Now that you're all set up, you're now ready to [get started](./template-application-rails/README.md). +Now that you're all set up, you're now ready to [get started](./app-rails/README.md). ## Learn more about this template diff --git a/app-rails/Makefile b/app-rails/Makefile index d92a229..4a46a5d 100644 --- a/app-rails/Makefile +++ b/app-rails/Makefile @@ -23,7 +23,7 @@ __check_defined = \ # Constants ################################################## -APP_NAME := template-application-rails +APP_NAME := app-rails # Support other container tools like `finch` ifdef CONTAINER_CMD diff --git a/app-rails/README.md b/app-rails/README.md index db80d42..755fbf9 100644 --- a/app-rails/README.md +++ b/app-rails/README.md @@ -17,7 +17,7 @@ This is a [Ruby on Rails](https://rubyonrails.org/) application. It includes: As a Rails app, much of the directory structure is driven by Rails conventions. We've also included directories for common patterns, such as adapters, form objects and services. -**[Refer to the Software Architecture doc for more detail](../docs/template-application-rails/software-architecture.md)** +**[Refer to the Software Architecture doc for more detail](../docs/app-rails/software-architecture.md)** Below are the primary directories to be aware of when working on the app: @@ -107,9 +107,9 @@ To run natively: ## 📇 Additional reading -Beyond this README, you should also refer to the [`docs` directory](../docs/template-application-rails) for more detailed info. Some highlights: +Beyond this README, you should also refer to the [`docs` directory](../docs/app-rails) for more detailed info. Some highlights: -- [Technical foundation](../docs/template-application-rails/technical-foundation.md) -- [Software architecture](../docs/template-application-rails/software-architecture.md) -- [Authentication & Authorization](../docs/template-application-rails/auth.md) -- [Internationalization (i18n)](../docs/template-application-rails/internationalization.md) +- [Technical foundation](../docs/app-rails/technical-foundation.md) +- [Software architecture](../docs/app-rails/software-architecture.md) +- [Authentication & Authorization](../docs/app-rails/auth.md) +- [Internationalization (i18n)](../docs/app-rails/internationalization.md) diff --git a/app-rails/config/cable.yml b/app-rails/config/cable.yml index 8979b1b..7e66857 100644 --- a/app-rails/config/cable.yml +++ b/app-rails/config/cable.yml @@ -7,4 +7,4 @@ test: production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> - channel_prefix: template_application_rails_production + channel_prefix: app_rails_production diff --git a/app-rails/config/database.yml b/app-rails/config/database.yml index efbee17..49a5ba1 100644 --- a/app-rails/config/database.yml +++ b/app-rails/config/database.yml @@ -34,7 +34,7 @@ development: # 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: template_application_rails + #username: app_rails # The password associated with the PostgreSQL role (username). #password: @@ -62,7 +62,7 @@ development: # Do not set this db to the same as development or production. test: <<: *default - database: template_application_rails_test + database: app_rails_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 diff --git a/app-rails/config/environments/production.rb b/app-rails/config/environments/production.rb index de6ef19..085748c 100644 --- a/app-rails/config/environments/production.rb +++ b/app-rails/config/environments/production.rb @@ -72,7 +72,7 @@ # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "template_application_rails_production" + # config.active_job.queue_name_prefix = "app_rails_production" config.action_mailer.delivery_method = :sesv2 config.action_mailer.perform_caching = false diff --git a/app-rails/local.env.example b/app-rails/local.env.example index 172c13e..ee8f7bb 100644 --- a/app-rails/local.env.example +++ b/app-rails/local.env.example @@ -33,6 +33,6 @@ COGNITO_CLIENT_ID= ############################ DB_HOST=127.0.0.1 -DB_NAME=template_application_rails -DB_USER=template_application_rails +DB_NAME=app_rails +DB_USER=app_rails DB_PASSWORD=secret123 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e303515..e69cb01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,9 +4,9 @@ services: command: postgres -c "log_lock_waits=on" -N 1000 -c "fsync=off" environment: POSTGRES_PASSWORD: secret123 - POSTGRES_USER: template_application_rails + POSTGRES_USER: app_rails healthcheck: - test: "pg_isready --username=template_application_rails" + test: "pg_isready --username=app_rails" timeout: 10s retries: 20 ports: @@ -17,14 +17,14 @@ services: # Rails app # Configured for "development" RAILS_ENV - template-application-rails: + app_rails: build: - context: ./app + context: ./app-rails target: dev depends_on: database: condition: service_healthy - env_file: ./app/.env + env_file: ./app-rails/.env environment: - DB_HOST=database - RAILS_BINDING=0.0.0.0 diff --git a/docs/template-application-rails/README.md b/docs/app-rails/README.md similarity index 69% rename from docs/template-application-rails/README.md rename to docs/app-rails/README.md index 85161d7..6b0ab43 100644 --- a/docs/template-application-rails/README.md +++ b/docs/app-rails/README.md @@ -1,4 +1,4 @@ -# Documentation for template-application-rails +# Documentation for app-rails ## 📂 Directory structure @@ -10,6 +10,6 @@ ## Recommended reading -- Start by reading the application's [main README](/template-application-rails/README.md) if you haven't already +- Start by reading the application's [main README](/app-rails/README.md) if you haven't already - Next, review the [Technical Foundation](./technical-foundation.md) - Continue reviewing the other documentation in this directory as desired \ No newline at end of file diff --git a/docs/template-application-rails/auth.md b/docs/app-rails/auth.md similarity index 100% rename from docs/template-application-rails/auth.md rename to docs/app-rails/auth.md diff --git a/docs/template-application-rails/decisions/README.md b/docs/app-rails/decisions/README.md similarity index 100% rename from docs/template-application-rails/decisions/README.md rename to docs/app-rails/decisions/README.md diff --git a/docs/template-application-rails/forms.md b/docs/app-rails/forms.md similarity index 100% rename from docs/template-application-rails/forms.md rename to docs/app-rails/forms.md diff --git a/docs/template-application-rails/internationalization.md b/docs/app-rails/internationalization.md similarity index 100% rename from docs/template-application-rails/internationalization.md rename to docs/app-rails/internationalization.md diff --git a/docs/template-application-rails/media/.keep b/docs/app-rails/media/.keep similarity index 100% rename from docs/template-application-rails/media/.keep rename to docs/app-rails/media/.keep diff --git a/docs/template-application-rails/software-architecture.md b/docs/app-rails/software-architecture.md similarity index 100% rename from docs/template-application-rails/software-architecture.md rename to docs/app-rails/software-architecture.md diff --git a/docs/template-application-rails/technical-foundation.md b/docs/app-rails/technical-foundation.md similarity index 100% rename from docs/template-application-rails/technical-foundation.md rename to docs/app-rails/technical-foundation.md diff --git a/init-postgres.sql b/init-postgres.sql index 8f92056..cd7fc3e 100644 --- a/init-postgres.sql +++ b/init-postgres.sql @@ -1,4 +1,4 @@ -- This is run when you start the Docker postgres container. -- Create separate databases for applications. -CREATE DATABASE template_application_rails; \ No newline at end of file +CREATE DATABASE app_rails; \ No newline at end of file From e2668b46d09ac9a7d761c9b1270031d28b9fffe6 Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 14:06:02 -0700 Subject: [PATCH 22/76] Add comment about database initialization script --- init-postgres.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/init-postgres.sql b/init-postgres.sql index cd7fc3e..1f1d5bd 100644 --- a/init-postgres.sql +++ b/init-postgres.sql @@ -1,4 +1,6 @@ -- This is run when you start the Docker postgres container. --- Create separate databases for applications. +-- Create separate databases for each application. +-- This only runs if the postgresql data directory is empty. +-- See the "Initialization scripts" section of https://hub.docker.com/_/postgres CREATE DATABASE app_rails; \ No newline at end of file From 3e901dac9b9057be680722c9a7e83a13eee31619 Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 14:06:30 -0700 Subject: [PATCH 23/76] Add template-only-* content from nextjs app template --- .../download-and-install-template.sh | 16 +++++++ template-only-bin/install-template.sh | 27 +++++++++++ template-only-bin/update-template.sh | 47 +++++++++++++++++++ template-only-docs/set-up-cd.md | 21 +++++++++ .../set-up-dependency-management.md | 17 +++++++ 5 files changed, 128 insertions(+) create mode 100755 template-only-bin/download-and-install-template.sh create mode 100755 template-only-bin/install-template.sh create mode 100755 template-only-bin/update-template.sh create mode 100644 template-only-docs/set-up-cd.md create mode 100644 template-only-docs/set-up-dependency-management.md diff --git a/template-only-bin/download-and-install-template.sh b/template-only-bin/download-and-install-template.sh new file mode 100755 index 0000000..aceeb55 --- /dev/null +++ b/template-only-bin/download-and-install-template.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -euo pipefail + +echo "Fetch latest version of template-application-nextjs" +git clone --single-branch --branch main --depth 1 git@github.com:navapbc/template-application-nextjs.git + +echo "Install template" +./template-application-nextjs/template-only-bin/install-template.sh + +# Store template version in a file +cd template-application-nextjs +git rev-parse HEAD >../.template-nextjs-version +cd - + +echo "Clean up template-application-nextjs folder" +rm -fr template-application-nextjs diff --git a/template-only-bin/install-template.sh b/template-only-bin/install-template.sh new file mode 100755 index 0000000..760ea23 --- /dev/null +++ b/template-only-bin/install-template.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# +# This script installs template-application-nextjs to your project. Run +# This script from your project's root directory. +set -euo pipefail + +CUR_DIR=$(pwd) +SCRIPT_DIR=$(dirname $0) +TEMPLATE_DIR="$SCRIPT_DIR/.." + +echo "Copy files from template-application-nextjs" +cd $TEMPLATE_DIR +# Note: Keep this list of paths in sync with INCLUDE_PATHS in update-template.sh +cp -r \ + .github \ + .grype.yml \ + app \ + docker-compose.yml \ + docs \ + $CUR_DIR +cd - + +echo "Remove files relevant only to template development" +# Note: Keep this list of paths in sync with EXCLUDE_OPT in update-template.sh +rm .github/workflows/template-only-* +rm -rf .github/ISSUE_TEMPLATE +rm -rf docs/decisions/template diff --git a/template-only-bin/update-template.sh b/template-only-bin/update-template.sh new file mode 100755 index 0000000..a15385d --- /dev/null +++ b/template-only-bin/update-template.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# ----------------------------------------------------------------------------- +# This script updates template-application-nextjs in your project. Run +# This script from your project's root directory. +# +# Positional parameters: +# TARGET_VERSION (optional) – the version of template-application-nextjs to upgrade to. +# Defaults to main. +# ----------------------------------------------------------------------------- +set -euo pipefail + +TARGET_VERSION=${1:-"main"} + +CURRENT_VERSION=$(cat .template-nextjs-version) + +echo "Clone template-application-nextjs" +git clone https://github.com/navapbc/template-application-nextjs.git + +echo "Creating patch" +cd template-application-nextjs +git checkout $TARGET_VERSION + +# Get version hash to update .template-nextjs-version after patch is successful +TARGET_VERSION_HASH=$(git rev-parse HEAD) + +# Note: Keep this list in sync with the files copied in install-template.sh +INCLUDE_PATHS=" \ + .github \ + .grype.yml \ + app \ + docker-compose.yml \ + docs" +git diff $CURRENT_VERSION $TARGET_VERSION -- $INCLUDE_PATHS >patch +cd - + +echo "Applying patch" +# Note: Keep this list in sync with the removed files in install-template.sh +EXCLUDE_OPT="--exclude=.github/workflows/template-only-* \ + --exclude=.github/ISSUE_TEMPLATE \ + --exclude=docs/decisions/template" +git apply $EXCLUDE_OPT --allow-empty template-application-nextjs/patch + +echo "Saving new template version to .template-application-nextjs" +echo $TARGET_VERSION_HASH >.template-nextjs-version + +echo "Clean up template-application-nextjs folder" +rm -fr template-application-nextjs diff --git a/template-only-docs/set-up-cd.md b/template-only-docs/set-up-cd.md new file mode 100644 index 0000000..1684001 --- /dev/null +++ b/template-only-docs/set-up-cd.md @@ -0,0 +1,21 @@ +# Deployments + +## Next.js app + +The Next.js app can be deployed as a static HTML export or as a Node.js server, depending on your needs. By default, the Next.js and Infra templates are configured to deploy the app as a Node.js server in a Docker container. [Reference the server rendering ADR for additional context](../docs/decisions/app/0005-server-rendering.md). + +The [Next.js deployment documentation](https://nextjs.org/docs/deployment) provides more information on the various ways to deploy a Next.js application. + +## Storybook + +Storybook can be deployed as a static HTML export. This template includes a GitHub workflow that will build and deploy Storybook to [GitHub Pages](https://pages.github.com/). To set up this workflow: + +1. You can enable continuous deployment of Storybook to GitHub Pages by searching for `!!` in `.github/workflows/deploy-storybook.yml` and uncommenting the `on: push: ["main"]` trigger. This will trigger the deployment workflow on every merge to `main`. +1. Select "GitHub Actions" from the **Source** dropdown field on the "Pages" tab in your repo settings (`Settings > Pages`). +1. Trigger the workflow from the repo's Actions tab or by pushing a commit to your main branch + +If your GitHub repo is public and you're using the default domain, Storybook will be hosted at a subdirectory: `https://.github.io//`. A `NEXT_PUBLIC_BASE_PATH` environment variable is available to your code so that any relative paths can be properly prefixed with the subdirectory where your Storybook is hosted. The Storybook configuration is already setup to use this environment variable, as long as the `basePath` in your `next.config.js` uses `process.env.NEXT_PUBLIC_BASE_PATH` as the value. + +### Alternatives + +One constraint of GitHub Pages is you only have one environment. The [Storybook deployment documentation](https://storybook.js.org/docs/react/sharing/publish-storybook) provides more information on other deployment options. diff --git a/template-only-docs/set-up-dependency-management.md b/template-only-docs/set-up-dependency-management.md new file mode 100644 index 0000000..ac28d05 --- /dev/null +++ b/template-only-docs/set-up-dependency-management.md @@ -0,0 +1,17 @@ +# Dependency Management with Renovate + +Out of the box this repo uses [Renovate](https://docs.renovatebot.com/) for dependency management. More information on the decision to try renovate can be found [here](https://github.com/navapbc/template-application-nextjs/blob/main/docs/decisions/0002-use-renovate-for-dependency-updates.md). Renovate is free and open-source and allows us to bundle dependency updates together and customize their scheduling. + +**Opting out of renovate**: + +If you decide you don't want to use renovate, you can delete the `renovate.json` file from the template code. If you plan to rely on [Dependabot](https://docs.github.com/en/code-security/dependabot), you'll likely want to add a `.github/dependabot.yml` file ([example here](https://github.com/navapbc/template-application-nextjs/blob/7ddb06b23524536db2e24bd43ec3ff7ec19d52bf/.github/dependabot.yml)) + +**Getting started with renovate**: + +1. Install Renovate's GitHub App for your repo ([Docs](https://docs.renovatebot.com/getting-started/installing-onboarding/#hosted-githubcom-app)). For most projects, you most likely only want to do this for your select repository. Note that if you prefer not to use the GitHub App, renovate does offer some alternatives including self-hosting. +2. After installation, Renovate should open a "Configure Renovate" pull request in your repository. Once you merge this PR, you will be all set. For more detail on this onboarding PR (what it contains, what to look for, what you can customize etc), check out [renovate's onboarding tutorial README for useful examples](https://github.com/renovatebot/tutorial). Note that this codebase comes with a `renovate.json` file out of the box with configuration options you can keep dependending on your preferences. +3. Note that Renovate can read GitHub's Vulnerability Alerts to customize pull requests. For this to work, you must enable Dependency graph and Dependabot alerts (This is covered in the 'other Github settings' section above, but mentioning here explicitly). For more detail on vulnerability management with renovate, [see here](https://github.com/renovatebot/renovate/blob/main/docs/usage/configuration-options.md#vulnerabilityalerts). + +**Beyond the basics**: + +After following the above steps your repository should be good to go with Renovate in terms of the basics. Future optimizations you may wish to look into include the [Renovate Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) and adding the renovate-config-validator program to validate any future renovate config changes prior to merge ([documentation here](https://docs.renovatebot.com/getting-started/installing-onboarding/#reconfigure-via-pr)). From 2c2adc350bac1277c54d7582895238d847e8ea9a Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 14:34:27 -0700 Subject: [PATCH 24/76] WIP template-only-bin --- .../download-and-install-template.sh | 22 +++++---- template-only-bin/install-template.sh | 48 +++++++++++-------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/template-only-bin/download-and-install-template.sh b/template-only-bin/download-and-install-template.sh index aceeb55..e39b721 100755 --- a/template-only-bin/download-and-install-template.sh +++ b/template-only-bin/download-and-install-template.sh @@ -1,16 +1,22 @@ -#!/bin/bash +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# This script installs a template application to your project. +# Run this script your project's root directory. +# ----------------------------------------------------------------------------- set -euo pipefail -echo "Fetch latest version of template-application-nextjs" -git clone --single-branch --branch main --depth 1 git@github.com:navapbc/template-application-nextjs.git +TEMPLATE_NAME="template-application-nextjs" + +echo "Fetch latest version of $(TEMPLATE_NAME)" +git clone --single-branch --branch main --depth 1 git@github.com:navapbc/$(TEMPLATE_NAME).git echo "Install template" -./template-application-nextjs/template-only-bin/install-template.sh +./$(TEMPLATE_NAME)/template-only-bin/install-template.sh $TEMPLATE_NAME # Store template version in a file -cd template-application-nextjs -git rev-parse HEAD >../.template-nextjs-version +cd $(TEMPLATE_NAME) +git rev-parse HEAD >../.$(TEMPLATE_NAME)-version cd - -echo "Clean up template-application-nextjs folder" -rm -fr template-application-nextjs +echo "Clean up $(TEMPLATE_NAME) folder" +rm -fr $(TEMPLATE_NAME) diff --git a/template-only-bin/install-template.sh b/template-only-bin/install-template.sh index 760ea23..7ddc616 100755 --- a/template-only-bin/install-template.sh +++ b/template-only-bin/install-template.sh @@ -1,27 +1,35 @@ -#!/bin/bash -# -# This script installs template-application-nextjs to your project. Run -# This script from your project's root directory. +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# This script installs a template application to your project. +# Run this script your project's root directory. +# ----------------------------------------------------------------------------- set -euo pipefail CUR_DIR=$(pwd) +TEMPLATE_NAME=$(basename $CUR_DIR) SCRIPT_DIR=$(dirname $0) TEMPLATE_DIR="$SCRIPT_DIR/.." -echo "Copy files from template-application-nextjs" -cd $TEMPLATE_DIR -# Note: Keep this list of paths in sync with INCLUDE_PATHS in update-template.sh -cp -r \ - .github \ - .grype.yml \ - app \ - docker-compose.yml \ - docs \ - $CUR_DIR -cd - +echo "CUR_DIR: $CUR_DIR" +echo "SCRIPT_DIR: $SCRIPT_DIR" +echo "TEMPLATE_DIR: $TEMPLATE_DIR" +echo "TEMPLATE_NAME: $TEMPLATE_NAME" -echo "Remove files relevant only to template development" -# Note: Keep this list of paths in sync with EXCLUDE_OPT in update-template.sh -rm .github/workflows/template-only-* -rm -rf .github/ISSUE_TEMPLATE -rm -rf docs/decisions/template +# echo "Copy files from $(TEMPLATE_NAME)" +# cd $TEMPLATE_DIR +# # Note: Keep this list of paths in sync with INCLUDE_PATHS in update-template.sh +# cp -r \ +# .github \ +# .gitignore \ +# .grype.yml \ +# app \ +# docker-compose.yml \ +# docs \ +# $CUR_DIR +# cd - + +# echo "Remove files relevant only to template development" +# # Note: Keep this list of paths in sync with EXCLUDE_OPT in update-template.sh +# rm .github/workflows/template-only-* +# rm -rf .github/ISSUE_TEMPLATE +# rm -rf docs/decisions/template From aef060cbc93b997c90dbb70c1780b4fe75164623 Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 14:36:53 -0700 Subject: [PATCH 25/76] Fix bad rename/rebase --- .../app/adapters/auth/cognito_adapter.rb | 0 {pfml => app-rails}/app/adapters/auth/errors.rb | 0 .../app/adapters/auth/mock_adapter.rb | 0 {pfml => app-rails}/app/assets/images/claimant.jpg | Bin {pfml => app-rails}/app/assets/images/employer.jpg | Bin .../app/forms/users/associate_mfa_form.rb | 0 .../app/forms/users/auth_app_code_form.rb | 0 .../app/forms/users/forgot_password_form.rb | 0 .../app/forms/users/mfa_preference_form.rb | 0 .../app/forms/users/new_session_form.rb | 0 .../app/forms/users/registration_form.rb | 0 .../app/forms/users/resend_verification_form.rb | 0 .../app/forms/users/reset_password_form.rb | 0 .../app/forms/users/update_email_form.rb | 0 .../app/forms/users/verify_account_form.rb | 0 {pfml => app-rails}/app/helpers/users/mfa_helper.rb | 0 .../app/policies/application_policy.rb | 0 {pfml => app-rails}/app/services/auth_service.rb | 0 .../app/services/notification_service.rb | 0 .../app/views/application/_breadcrumbs.html.erb | 0 .../app/views/application/_flash.html.erb | 0 .../app/views/application/_header.html.erb | 0 .../app/views/application/_language-toggle.html.erb | 0 .../app/views/application/sandbox.html.erb | 0 {pfml => app-rails}/app/views/home/index.html.erb | 0 .../app/views/users/accounts/edit.html.erb | 0 .../app/views/users/mfa/new.html.erb | 0 .../app/views/users/mfa/preference.html.erb | 0 .../app/views/users/passwords/forgot.html.erb | 0 .../app/views/users/passwords/reset.html.erb | 0 .../app/views/users/registrations/new.html.erb | 0 .../registrations/new_account_verification.html.erb | 0 .../app/views/users/sessions/challenge.html.erb | 0 .../app/views/users/sessions/new.html.erb | 0 .../spec/adapters/cognito_adapter_spec.rb | 0 .../spec/controllers/home_controller_spec.rb | 0 .../controllers/users/accounts_controller_spec.rb | 0 .../spec/controllers/users/mfa_controller_spec.rb | 0 .../controllers/users/passwords_controller_spec.rb | 0 .../users/registrations_controller_spec.rb | 0 .../controllers/users/sessions_controller_spec.rb | 0 .../spec/factories/user_role_factory.rb | 0 {pfml => app-rails}/spec/factories/users_factory.rb | 0 .../spec/forms/users/new_session_form_spec.rb | 0 .../spec/forms/users/registration_form_spec.rb | 0 .../spec/forms/users/verify_account_form_spec.rb | 0 {pfml => app-rails}/spec/models/user_spec.rb | 0 {pfml => app-rails}/spec/rails_helper.rb | 0 .../spec/services/auth_service_spec.rb | 0 {pfml => app-rails}/spec/spec_helper.rb | 0 {pfml => app-rails}/spec/support/factory_bot.rb | 0 51 files changed, 0 insertions(+), 0 deletions(-) rename {pfml => app-rails}/app/adapters/auth/cognito_adapter.rb (100%) rename {pfml => app-rails}/app/adapters/auth/errors.rb (100%) rename {pfml => app-rails}/app/adapters/auth/mock_adapter.rb (100%) rename {pfml => app-rails}/app/assets/images/claimant.jpg (100%) rename {pfml => app-rails}/app/assets/images/employer.jpg (100%) rename {pfml => app-rails}/app/forms/users/associate_mfa_form.rb (100%) rename {pfml => app-rails}/app/forms/users/auth_app_code_form.rb (100%) rename {pfml => app-rails}/app/forms/users/forgot_password_form.rb (100%) rename {pfml => app-rails}/app/forms/users/mfa_preference_form.rb (100%) rename {pfml => app-rails}/app/forms/users/new_session_form.rb (100%) rename {pfml => app-rails}/app/forms/users/registration_form.rb (100%) rename {pfml => app-rails}/app/forms/users/resend_verification_form.rb (100%) rename {pfml => app-rails}/app/forms/users/reset_password_form.rb (100%) rename {pfml => app-rails}/app/forms/users/update_email_form.rb (100%) rename {pfml => app-rails}/app/forms/users/verify_account_form.rb (100%) rename {pfml => app-rails}/app/helpers/users/mfa_helper.rb (100%) rename {pfml => app-rails}/app/policies/application_policy.rb (100%) rename {pfml => app-rails}/app/services/auth_service.rb (100%) rename {pfml => app-rails}/app/services/notification_service.rb (100%) rename {pfml => app-rails}/app/views/application/_breadcrumbs.html.erb (100%) rename {pfml => app-rails}/app/views/application/_flash.html.erb (100%) rename {pfml => app-rails}/app/views/application/_header.html.erb (100%) rename {pfml => app-rails}/app/views/application/_language-toggle.html.erb (100%) rename {pfml => app-rails}/app/views/application/sandbox.html.erb (100%) rename {pfml => app-rails}/app/views/home/index.html.erb (100%) rename {pfml => app-rails}/app/views/users/accounts/edit.html.erb (100%) rename {pfml => app-rails}/app/views/users/mfa/new.html.erb (100%) rename {pfml => app-rails}/app/views/users/mfa/preference.html.erb (100%) rename {pfml => app-rails}/app/views/users/passwords/forgot.html.erb (100%) rename {pfml => app-rails}/app/views/users/passwords/reset.html.erb (100%) rename {pfml => app-rails}/app/views/users/registrations/new.html.erb (100%) rename {pfml => app-rails}/app/views/users/registrations/new_account_verification.html.erb (100%) rename {pfml => app-rails}/app/views/users/sessions/challenge.html.erb (100%) rename {pfml => app-rails}/app/views/users/sessions/new.html.erb (100%) rename {pfml => app-rails}/spec/adapters/cognito_adapter_spec.rb (100%) rename {pfml => app-rails}/spec/controllers/home_controller_spec.rb (100%) rename {pfml => app-rails}/spec/controllers/users/accounts_controller_spec.rb (100%) rename {pfml => app-rails}/spec/controllers/users/mfa_controller_spec.rb (100%) rename {pfml => app-rails}/spec/controllers/users/passwords_controller_spec.rb (100%) rename {pfml => app-rails}/spec/controllers/users/registrations_controller_spec.rb (100%) rename {pfml => app-rails}/spec/controllers/users/sessions_controller_spec.rb (100%) rename {pfml => app-rails}/spec/factories/user_role_factory.rb (100%) rename {pfml => app-rails}/spec/factories/users_factory.rb (100%) rename {pfml => app-rails}/spec/forms/users/new_session_form_spec.rb (100%) rename {pfml => app-rails}/spec/forms/users/registration_form_spec.rb (100%) rename {pfml => app-rails}/spec/forms/users/verify_account_form_spec.rb (100%) rename {pfml => app-rails}/spec/models/user_spec.rb (100%) rename {pfml => app-rails}/spec/rails_helper.rb (100%) rename {pfml => app-rails}/spec/services/auth_service_spec.rb (100%) rename {pfml => app-rails}/spec/spec_helper.rb (100%) rename {pfml => app-rails}/spec/support/factory_bot.rb (100%) diff --git a/pfml/app/adapters/auth/cognito_adapter.rb b/app-rails/app/adapters/auth/cognito_adapter.rb similarity index 100% rename from pfml/app/adapters/auth/cognito_adapter.rb rename to app-rails/app/adapters/auth/cognito_adapter.rb diff --git a/pfml/app/adapters/auth/errors.rb b/app-rails/app/adapters/auth/errors.rb similarity index 100% rename from pfml/app/adapters/auth/errors.rb rename to app-rails/app/adapters/auth/errors.rb diff --git a/pfml/app/adapters/auth/mock_adapter.rb b/app-rails/app/adapters/auth/mock_adapter.rb similarity index 100% rename from pfml/app/adapters/auth/mock_adapter.rb rename to app-rails/app/adapters/auth/mock_adapter.rb diff --git a/pfml/app/assets/images/claimant.jpg b/app-rails/app/assets/images/claimant.jpg similarity index 100% rename from pfml/app/assets/images/claimant.jpg rename to app-rails/app/assets/images/claimant.jpg diff --git a/pfml/app/assets/images/employer.jpg b/app-rails/app/assets/images/employer.jpg similarity index 100% rename from pfml/app/assets/images/employer.jpg rename to app-rails/app/assets/images/employer.jpg diff --git a/pfml/app/forms/users/associate_mfa_form.rb b/app-rails/app/forms/users/associate_mfa_form.rb similarity index 100% rename from pfml/app/forms/users/associate_mfa_form.rb rename to app-rails/app/forms/users/associate_mfa_form.rb diff --git a/pfml/app/forms/users/auth_app_code_form.rb b/app-rails/app/forms/users/auth_app_code_form.rb similarity index 100% rename from pfml/app/forms/users/auth_app_code_form.rb rename to app-rails/app/forms/users/auth_app_code_form.rb diff --git a/pfml/app/forms/users/forgot_password_form.rb b/app-rails/app/forms/users/forgot_password_form.rb similarity index 100% rename from pfml/app/forms/users/forgot_password_form.rb rename to app-rails/app/forms/users/forgot_password_form.rb diff --git a/pfml/app/forms/users/mfa_preference_form.rb b/app-rails/app/forms/users/mfa_preference_form.rb similarity index 100% rename from pfml/app/forms/users/mfa_preference_form.rb rename to app-rails/app/forms/users/mfa_preference_form.rb diff --git a/pfml/app/forms/users/new_session_form.rb b/app-rails/app/forms/users/new_session_form.rb similarity index 100% rename from pfml/app/forms/users/new_session_form.rb rename to app-rails/app/forms/users/new_session_form.rb diff --git a/pfml/app/forms/users/registration_form.rb b/app-rails/app/forms/users/registration_form.rb similarity index 100% rename from pfml/app/forms/users/registration_form.rb rename to app-rails/app/forms/users/registration_form.rb diff --git a/pfml/app/forms/users/resend_verification_form.rb b/app-rails/app/forms/users/resend_verification_form.rb similarity index 100% rename from pfml/app/forms/users/resend_verification_form.rb rename to app-rails/app/forms/users/resend_verification_form.rb diff --git a/pfml/app/forms/users/reset_password_form.rb b/app-rails/app/forms/users/reset_password_form.rb similarity index 100% rename from pfml/app/forms/users/reset_password_form.rb rename to app-rails/app/forms/users/reset_password_form.rb diff --git a/pfml/app/forms/users/update_email_form.rb b/app-rails/app/forms/users/update_email_form.rb similarity index 100% rename from pfml/app/forms/users/update_email_form.rb rename to app-rails/app/forms/users/update_email_form.rb diff --git a/pfml/app/forms/users/verify_account_form.rb b/app-rails/app/forms/users/verify_account_form.rb similarity index 100% rename from pfml/app/forms/users/verify_account_form.rb rename to app-rails/app/forms/users/verify_account_form.rb diff --git a/pfml/app/helpers/users/mfa_helper.rb b/app-rails/app/helpers/users/mfa_helper.rb similarity index 100% rename from pfml/app/helpers/users/mfa_helper.rb rename to app-rails/app/helpers/users/mfa_helper.rb diff --git a/pfml/app/policies/application_policy.rb b/app-rails/app/policies/application_policy.rb similarity index 100% rename from pfml/app/policies/application_policy.rb rename to app-rails/app/policies/application_policy.rb diff --git a/pfml/app/services/auth_service.rb b/app-rails/app/services/auth_service.rb similarity index 100% rename from pfml/app/services/auth_service.rb rename to app-rails/app/services/auth_service.rb diff --git a/pfml/app/services/notification_service.rb b/app-rails/app/services/notification_service.rb similarity index 100% rename from pfml/app/services/notification_service.rb rename to app-rails/app/services/notification_service.rb diff --git a/pfml/app/views/application/_breadcrumbs.html.erb b/app-rails/app/views/application/_breadcrumbs.html.erb similarity index 100% rename from pfml/app/views/application/_breadcrumbs.html.erb rename to app-rails/app/views/application/_breadcrumbs.html.erb diff --git a/pfml/app/views/application/_flash.html.erb b/app-rails/app/views/application/_flash.html.erb similarity index 100% rename from pfml/app/views/application/_flash.html.erb rename to app-rails/app/views/application/_flash.html.erb diff --git a/pfml/app/views/application/_header.html.erb b/app-rails/app/views/application/_header.html.erb similarity index 100% rename from pfml/app/views/application/_header.html.erb rename to app-rails/app/views/application/_header.html.erb diff --git a/pfml/app/views/application/_language-toggle.html.erb b/app-rails/app/views/application/_language-toggle.html.erb similarity index 100% rename from pfml/app/views/application/_language-toggle.html.erb rename to app-rails/app/views/application/_language-toggle.html.erb diff --git a/pfml/app/views/application/sandbox.html.erb b/app-rails/app/views/application/sandbox.html.erb similarity index 100% rename from pfml/app/views/application/sandbox.html.erb rename to app-rails/app/views/application/sandbox.html.erb diff --git a/pfml/app/views/home/index.html.erb b/app-rails/app/views/home/index.html.erb similarity index 100% rename from pfml/app/views/home/index.html.erb rename to app-rails/app/views/home/index.html.erb diff --git a/pfml/app/views/users/accounts/edit.html.erb b/app-rails/app/views/users/accounts/edit.html.erb similarity index 100% rename from pfml/app/views/users/accounts/edit.html.erb rename to app-rails/app/views/users/accounts/edit.html.erb diff --git a/pfml/app/views/users/mfa/new.html.erb b/app-rails/app/views/users/mfa/new.html.erb similarity index 100% rename from pfml/app/views/users/mfa/new.html.erb rename to app-rails/app/views/users/mfa/new.html.erb diff --git a/pfml/app/views/users/mfa/preference.html.erb b/app-rails/app/views/users/mfa/preference.html.erb similarity index 100% rename from pfml/app/views/users/mfa/preference.html.erb rename to app-rails/app/views/users/mfa/preference.html.erb diff --git a/pfml/app/views/users/passwords/forgot.html.erb b/app-rails/app/views/users/passwords/forgot.html.erb similarity index 100% rename from pfml/app/views/users/passwords/forgot.html.erb rename to app-rails/app/views/users/passwords/forgot.html.erb diff --git a/pfml/app/views/users/passwords/reset.html.erb b/app-rails/app/views/users/passwords/reset.html.erb similarity index 100% rename from pfml/app/views/users/passwords/reset.html.erb rename to app-rails/app/views/users/passwords/reset.html.erb diff --git a/pfml/app/views/users/registrations/new.html.erb b/app-rails/app/views/users/registrations/new.html.erb similarity index 100% rename from pfml/app/views/users/registrations/new.html.erb rename to app-rails/app/views/users/registrations/new.html.erb diff --git a/pfml/app/views/users/registrations/new_account_verification.html.erb b/app-rails/app/views/users/registrations/new_account_verification.html.erb similarity index 100% rename from pfml/app/views/users/registrations/new_account_verification.html.erb rename to app-rails/app/views/users/registrations/new_account_verification.html.erb diff --git a/pfml/app/views/users/sessions/challenge.html.erb b/app-rails/app/views/users/sessions/challenge.html.erb similarity index 100% rename from pfml/app/views/users/sessions/challenge.html.erb rename to app-rails/app/views/users/sessions/challenge.html.erb diff --git a/pfml/app/views/users/sessions/new.html.erb b/app-rails/app/views/users/sessions/new.html.erb similarity index 100% rename from pfml/app/views/users/sessions/new.html.erb rename to app-rails/app/views/users/sessions/new.html.erb diff --git a/pfml/spec/adapters/cognito_adapter_spec.rb b/app-rails/spec/adapters/cognito_adapter_spec.rb similarity index 100% rename from pfml/spec/adapters/cognito_adapter_spec.rb rename to app-rails/spec/adapters/cognito_adapter_spec.rb diff --git a/pfml/spec/controllers/home_controller_spec.rb b/app-rails/spec/controllers/home_controller_spec.rb similarity index 100% rename from pfml/spec/controllers/home_controller_spec.rb rename to app-rails/spec/controllers/home_controller_spec.rb diff --git a/pfml/spec/controllers/users/accounts_controller_spec.rb b/app-rails/spec/controllers/users/accounts_controller_spec.rb similarity index 100% rename from pfml/spec/controllers/users/accounts_controller_spec.rb rename to app-rails/spec/controllers/users/accounts_controller_spec.rb diff --git a/pfml/spec/controllers/users/mfa_controller_spec.rb b/app-rails/spec/controllers/users/mfa_controller_spec.rb similarity index 100% rename from pfml/spec/controllers/users/mfa_controller_spec.rb rename to app-rails/spec/controllers/users/mfa_controller_spec.rb diff --git a/pfml/spec/controllers/users/passwords_controller_spec.rb b/app-rails/spec/controllers/users/passwords_controller_spec.rb similarity index 100% rename from pfml/spec/controllers/users/passwords_controller_spec.rb rename to app-rails/spec/controllers/users/passwords_controller_spec.rb diff --git a/pfml/spec/controllers/users/registrations_controller_spec.rb b/app-rails/spec/controllers/users/registrations_controller_spec.rb similarity index 100% rename from pfml/spec/controllers/users/registrations_controller_spec.rb rename to app-rails/spec/controllers/users/registrations_controller_spec.rb diff --git a/pfml/spec/controllers/users/sessions_controller_spec.rb b/app-rails/spec/controllers/users/sessions_controller_spec.rb similarity index 100% rename from pfml/spec/controllers/users/sessions_controller_spec.rb rename to app-rails/spec/controllers/users/sessions_controller_spec.rb diff --git a/pfml/spec/factories/user_role_factory.rb b/app-rails/spec/factories/user_role_factory.rb similarity index 100% rename from pfml/spec/factories/user_role_factory.rb rename to app-rails/spec/factories/user_role_factory.rb diff --git a/pfml/spec/factories/users_factory.rb b/app-rails/spec/factories/users_factory.rb similarity index 100% rename from pfml/spec/factories/users_factory.rb rename to app-rails/spec/factories/users_factory.rb diff --git a/pfml/spec/forms/users/new_session_form_spec.rb b/app-rails/spec/forms/users/new_session_form_spec.rb similarity index 100% rename from pfml/spec/forms/users/new_session_form_spec.rb rename to app-rails/spec/forms/users/new_session_form_spec.rb diff --git a/pfml/spec/forms/users/registration_form_spec.rb b/app-rails/spec/forms/users/registration_form_spec.rb similarity index 100% rename from pfml/spec/forms/users/registration_form_spec.rb rename to app-rails/spec/forms/users/registration_form_spec.rb diff --git a/pfml/spec/forms/users/verify_account_form_spec.rb b/app-rails/spec/forms/users/verify_account_form_spec.rb similarity index 100% rename from pfml/spec/forms/users/verify_account_form_spec.rb rename to app-rails/spec/forms/users/verify_account_form_spec.rb diff --git a/pfml/spec/models/user_spec.rb b/app-rails/spec/models/user_spec.rb similarity index 100% rename from pfml/spec/models/user_spec.rb rename to app-rails/spec/models/user_spec.rb diff --git a/pfml/spec/rails_helper.rb b/app-rails/spec/rails_helper.rb similarity index 100% rename from pfml/spec/rails_helper.rb rename to app-rails/spec/rails_helper.rb diff --git a/pfml/spec/services/auth_service_spec.rb b/app-rails/spec/services/auth_service_spec.rb similarity index 100% rename from pfml/spec/services/auth_service_spec.rb rename to app-rails/spec/services/auth_service_spec.rb diff --git a/pfml/spec/spec_helper.rb b/app-rails/spec/spec_helper.rb similarity index 100% rename from pfml/spec/spec_helper.rb rename to app-rails/spec/spec_helper.rb diff --git a/pfml/spec/support/factory_bot.rb b/app-rails/spec/support/factory_bot.rb similarity index 100% rename from pfml/spec/support/factory_bot.rb rename to app-rails/spec/support/factory_bot.rb From cfb02a0ce2bb1f8181a4150f409a043757dd92c4 Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 14:58:53 -0700 Subject: [PATCH 26/76] *Update download-and-install-template.sh --- .../download-and-install-template.sh | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/template-only-bin/download-and-install-template.sh b/template-only-bin/download-and-install-template.sh index e39b721..e2eaa57 100755 --- a/template-only-bin/download-and-install-template.sh +++ b/template-only-bin/download-and-install-template.sh @@ -1,22 +1,34 @@ #!/usr/bin/env bash # ----------------------------------------------------------------------------- -# This script installs a template application to your project. -# Run this script your project's root directory. +# This script installs an application template to your project. +# Run this script in your project's root directory. +# +# Positional parameters: +# TARGET_VERSION (optional) – the version of the template application to install. +# Defaults to main. Can be any target that can be checked out, including a branch, +# version tag, or commit hash. # ----------------------------------------------------------------------------- set -euo pipefail -TEMPLATE_NAME="template-application-nextjs" +TARGET_VERSION=${1:-"main"} +TEMPLATE_NAME="template-application-rails" -echo "Fetch latest version of $(TEMPLATE_NAME)" -git clone --single-branch --branch main --depth 1 git@github.com:navapbc/$(TEMPLATE_NAME).git +git clone "https://github.com/navapbc/$TEMPLATE_NAME.git" +cd "$TEMPLATE_NAME" -echo "Install template" -./$(TEMPLATE_NAME)/template-only-bin/install-template.sh $TEMPLATE_NAME +echo "Checking out $TARGET_VERSION..." +git checkout "$TARGET_VERSION" +cd - &> /dev/null -# Store template version in a file -cd $(TEMPLATE_NAME) -git rev-parse HEAD >../.$(TEMPLATE_NAME)-version -cd - +echo "Installing $TEMPLATE_NAME..." +"./$TEMPLATE_NAME/template-only-bin/install-template.sh" "$TEMPLATE_NAME" -echo "Clean up $(TEMPLATE_NAME) folder" -rm -fr $(TEMPLATE_NAME) +echo "Storing template version in a file..." +cd "$TEMPLATE_NAME" +git rev-parse HEAD >../".$TEMPLATE_NAME-version" +cd - &> /dev/null + +echo "Cleaning up $TEMPLATE_NAME folder..." +rm -fr "$TEMPLATE_NAME" + +echo "...Done." \ No newline at end of file From 8458788b875ffa6e6df010dc11c88b1199a11429 Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 15:25:06 -0700 Subject: [PATCH 27/76] *Update install-template.sh --- template-only-bin/install-template.sh | 52 ++++++++++++++------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/template-only-bin/install-template.sh b/template-only-bin/install-template.sh index 7ddc616..e52b4a8 100755 --- a/template-only-bin/install-template.sh +++ b/template-only-bin/install-template.sh @@ -1,35 +1,37 @@ #!/usr/bin/env bash # ----------------------------------------------------------------------------- -# This script installs a template application to your project. -# Run this script your project's root directory. +# This script installs an application template to your project. +# Run this script using ./download-and-install-template.sh +# +# Positional parameters: +# TEMPLATE_NAME – the name of the template to install # ----------------------------------------------------------------------------- set -euo pipefail CUR_DIR=$(pwd) -TEMPLATE_NAME=$(basename $CUR_DIR) SCRIPT_DIR=$(dirname $0) TEMPLATE_DIR="$SCRIPT_DIR/.." +TEMPLATE_NAME=$1 +# Use shell parameter expansion to get the last word, where the delimiter between +# words is `-`. +# See https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Shell-Parameter-Expansion +TEMPLATE_SHORT_NAME="app-${TEMPLATE_NAME##*-}" -echo "CUR_DIR: $CUR_DIR" -echo "SCRIPT_DIR: $SCRIPT_DIR" -echo "TEMPLATE_DIR: $TEMPLATE_DIR" -echo "TEMPLATE_NAME: $TEMPLATE_NAME" +echo "Copying files from $TEMPLATE_NAME..." +cd $TEMPLATE_DIR +# Note: Keep this list of paths in sync with INCLUDE_PATHS in update-template.sh +# @TODO: Add .grype.yml +cp -r \ + .github \ + .gitignore \ + $TEMPLATE_SHORT_NAME \ + docker-compose.yml \ + init-postgres.sql \ + docs \ + $CUR_DIR +cd - >& /dev/null -# echo "Copy files from $(TEMPLATE_NAME)" -# cd $TEMPLATE_DIR -# # Note: Keep this list of paths in sync with INCLUDE_PATHS in update-template.sh -# cp -r \ -# .github \ -# .gitignore \ -# .grype.yml \ -# app \ -# docker-compose.yml \ -# docs \ -# $CUR_DIR -# cd - - -# echo "Remove files relevant only to template development" -# # Note: Keep this list of paths in sync with EXCLUDE_OPT in update-template.sh -# rm .github/workflows/template-only-* -# rm -rf .github/ISSUE_TEMPLATE -# rm -rf docs/decisions/template +echo "Removing files relevant only to template development..." +# Note: Keep this list of paths in sync with EXCLUDE_OPT in update-template.sh +rm -rf .github/workflows/template-only-* +rm -rf .github/ISSUE_TEMPLATE From 57d999c93e9c44272b8754b69dd7853b4adad162 Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 15:38:23 -0700 Subject: [PATCH 28/76] *Update update-template.sh --- template-only-bin/update-template.sh | 64 +++++++++++++++++----------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/template-only-bin/update-template.sh b/template-only-bin/update-template.sh index a15385d..40c4591 100755 --- a/template-only-bin/update-template.sh +++ b/template-only-bin/update-template.sh @@ -1,47 +1,59 @@ -#!/bin/bash +#!/usr/bin/env bash # ----------------------------------------------------------------------------- -# This script updates template-application-nextjs in your project. Run -# This script from your project's root directory. +# This script updates an application template in your project. +# Run this script in your project's root directory. # # Positional parameters: -# TARGET_VERSION (optional) – the version of template-application-nextjs to upgrade to. -# Defaults to main. +# TEMPLATE_NAME (required) – the name of the template to update. Should be in the +# format `template-application-`. This is case sensitive and must match +# the name of the application template git repo name. +# +# TARGET_VERSION (optional) – the version of the template application to install. +# Defaults to main. Can be any target that can be checked out, including a branch, +# version tag, or commit hash. # ----------------------------------------------------------------------------- set -euo pipefail -TARGET_VERSION=${1:-"main"} - -CURRENT_VERSION=$(cat .template-nextjs-version) +TEMPLATE_NAME=$1 +TARGET_VERSION=${2:-"main"} +CURRENT_VERSION=$(cat ".$TEMPLATE_NAME-version") +# Use shell parameter expansion to get the last word, where the delimiter between +# words is `-`. +# See https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Shell-Parameter-Expansion +TEMPLATE_SHORT_NAME="app-${TEMPLATE_NAME##*-}" -echo "Clone template-application-nextjs" -git clone https://github.com/navapbc/template-application-nextjs.git +echo "Cloning $TEMPLATE_NAME..." +git clone "https://github.com/navapbc/$TEMPLATE_NAME.git" -echo "Creating patch" -cd template-application-nextjs -git checkout $TARGET_VERSION +echo "Creating patch..." +cd "$TEMPLATE_NAME" +git checkout "$TARGET_VERSION" -# Get version hash to update .template-nextjs-version after patch is successful +# Get version hash to update version file with after patch is successful TARGET_VERSION_HASH=$(git rev-parse HEAD) # Note: Keep this list in sync with the files copied in install-template.sh +# @TODO: Add .grype.yml INCLUDE_PATHS=" \ .github \ - .grype.yml \ - app \ + .gitignore \ + $TEMPLATE_SHORT_NAME \ docker-compose.yml \ + init-postgres.sql \ docs" -git diff $CURRENT_VERSION $TARGET_VERSION -- $INCLUDE_PATHS >patch -cd - +git diff "$CURRENT_VERSION" "$TARGET_VERSION" -- "$INCLUDE_PATHS" >patch +cd - >& /dev/null -echo "Applying patch" +echo "Applying patch..." # Note: Keep this list in sync with the removed files in install-template.sh EXCLUDE_OPT="--exclude=.github/workflows/template-only-* \ - --exclude=.github/ISSUE_TEMPLATE \ - --exclude=docs/decisions/template" -git apply $EXCLUDE_OPT --allow-empty template-application-nextjs/patch + --exclude=.github/ISSUE_TEMPLATE" +git apply "$EXCLUDE_OPT" --allow-empty "$TEMPLATE_NAME/patch" + +echo "Saving new template version to .$TEMPLATE_NAME-version..." +echo "$TARGET_VERSION_HASH" >".$TEMPLATE_NAME-version" -echo "Saving new template version to .template-application-nextjs" -echo $TARGET_VERSION_HASH >.template-nextjs-version +echo "Cleaning up $TEMPLATE_NAME folder..." +rm -rf "$TEMPLATE_NAME" -echo "Clean up template-application-nextjs folder" -rm -fr template-application-nextjs +echo "...Done." From db3b326c1f403375460f8a6072190b999b0ad41a Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 15:41:51 -0700 Subject: [PATCH 29/76] Copy template-application-nextjs issue templates --- .github/ISSUE_TEMPLATE/bug.yaml | 37 +++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature.yaml | 32 +++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/task.yaml | 9 +++++++ 3 files changed, 78 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature.yaml create mode 100644 .github/ISSUE_TEMPLATE/task.yaml diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 0000000..9bcb235 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,37 @@ +name: Bug report +description: File a bug report +labels: ["bug", "triage"] +projects: ["navapbc/4"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce + description: Share the steps or a link to a repository we can use to reproduce the problem. + - type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + multiple: true + options: + - Not applicable + - Firefox + - Chrome + - Safari + - Microsoft Edge + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 0000000..dc310e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,32 @@ +name: Feature request +description: Suggest an idea for this project +labels: ["triage", "feature"] +projects: ["navapbc/4"] +body: + - type: textarea + attributes: + label: Describe the problem and the solution you'd like + description: A clear and concise description of what the problem is and what you want to happen. + value: | + **Is your feature request related to a problem? Please describe.** + + + **Describe the solution you'd like** + + + **Describe alternatives you've considered** + + - type: textarea + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + - type: dropdown + attributes: + label: Priority + description: How impactful would this be for your project? + multiple: false + options: + - "My project needs this now" + - "I anticipate needing this soon" + - "Nice to have" + - "Just want to discuss" diff --git a/.github/ISSUE_TEMPLATE/task.yaml b/.github/ISSUE_TEMPLATE/task.yaml new file mode 100644 index 0000000..48d101d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yaml @@ -0,0 +1,9 @@ +name: Task +description: Create a task for the team +projects: ["navapbc/4"] +body: + - type: textarea + attributes: + label: What's the task? + validations: + required: true From e7b8c48baa10e417f422ed1f0c44d5d3e0e68929 Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 11 Apr 2024 15:55:02 -0700 Subject: [PATCH 30/76] *Update README with new template-only-bin instructions --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 1ca77a1..f7d4088 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,12 @@ To get started using the template application on your project: 1. Clone the template repository 2. Copy the template files into your project directory 3. Remove any files specific to the template repository, like this README. + + You can optionally pass in a branch, commit hash, or release that you want to install. For example: + + ```bash + curl https://raw.githubusercontent.com/navapbc/template-application-rails/main/template-only-bin/download-and-install-template.sh | bash -s -- + ``` 2. [Follow the steps in `app-rails/README.md`](./app-rails/README.md) to set up the application locally. 3. Optional, if using the Platform infra template: [Follow the steps in the `template-infra` README](https://github.com/navapbc/template-infra#installation) to set up the various pieces of your infrastructure. @@ -49,6 +55,24 @@ To get started using the template application on your project: Now that you're all set up, you're now ready to [get started](./app-rails/README.md). +## Updates + +If you have previously installed this template and would like to update your project to use a newer version of this application: + +1. Run the [update script](./template-only-bin/update-template.sh) in your project's root directory and pass in the name of this template and the branch, commit hash, or release that you want to update to. + + ```bash + curl https://raw.githubusercontent.com/navapbc/template-application-rails/main/template-only-bin/update-template.sh | bash -s -- template-application-rails + ``` + + This script will: + + 1. Clone the template repository + 2. Copy the template files into your project directory + 3. Remove any files specific to the template repository, like this README. + +⚠ïļ Warning! This will overwrite existing files. Review all changes carefully. + ## Learn more about this template - [Dependency management](./template-only-docs/set-up-dependency-management.md) From 258823d02e81adc29861b42344254d120f2d8167 Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 17 Apr 2024 18:04:01 -0700 Subject: [PATCH 31/76] *Update update-template.sh --- template-only-bin/install-template.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template-only-bin/install-template.sh b/template-only-bin/install-template.sh index e52b4a8..1ec5964 100755 --- a/template-only-bin/install-template.sh +++ b/template-only-bin/install-template.sh @@ -4,7 +4,7 @@ # Run this script using ./download-and-install-template.sh # # Positional parameters: -# TEMPLATE_NAME – the name of the template to install +# TEMPLATE_NAME (required) – the name of the template to install # ----------------------------------------------------------------------------- set -euo pipefail From b961650a28b0d8b80be2ac081ca0a84b60ff7cdd Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 17 Apr 2024 18:04:11 -0700 Subject: [PATCH 32/76] *Add rename-template-app.sh --- template-only-bin/rename-template-app.sh | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100755 template-only-bin/rename-template-app.sh diff --git a/template-only-bin/rename-template-app.sh b/template-only-bin/rename-template-app.sh new file mode 100755 index 0000000..b3df10f --- /dev/null +++ b/template-only-bin/rename-template-app.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# This script renames the application template in your project. +# Run this script in your project's root directory. +# +# Positional parameters: +# NEW_NAME (required) - the new name for the application, in either snake- or kebab-case +# OLD_NAME (optional) – the old name for the application, in either snake- or kebab-case +# Defaults to the template's short name (e.g. app-rails) +# ----------------------------------------------------------------------------- +set -euo pipefail + +# Helper to get the correct sed -i behavior for both GNU sed and BSD sed (installed by default on macOS) +# Hat tip: https://stackoverflow.com/a/38595160 +sedi () { + sed --version >/dev/null 2>&1 && sed -i -- "$@" || sed -i "" "$@" +} +# Export the function so it can be used in the `find -exec` calls later on +export -f sedi + +NEW_NAME=$1 +OLD_NAME=${2:-"app-rails"} + +# Don't make assumptions about whether the arguments are snake- or kebab-case. Handle both. +# Get kebab-case names +OLD_NAME_KEBAB=$(echo $OLD_NAME | tr "_" "-") +NEW_NAME_KEBAB=$(echo $NEW_NAME | tr "_" "-") + +# Get snake-case names +OLD_NAME_SNAKE=$(echo $OLD_NAME | tr "-" "_") +NEW_NAME_SNAKE=$(echo $NEW_NAME | tr "-" "_") + +# Rename the app directory +if [ -d "$OLD_NAME" ]; then + echo "Renaming $OLD_NAME to $NEW_NAME..." + mv "$OLD_NAME" "$NEW_NAME" +fi + +# Rename all kebab-case instances +echo "Performing a find-and-replace for all instances of '$OLD_NAME_KEBAB' with '$NEW_NAME_KEBAB'..." +LC_ALL=C find . -type f -exec bash -c "sedi \"s/$OLD_NAME_KEBAB/$NEW_NAME_KEBAB/g\" \"{}\"" \; + +# Rename all snake-case instances +echo "Performing a find-and-replace for all instances of '$OLD_NAME_SNAKE' with '$NEW_NAME_SNAKE'..." +LC_ALL=C find . -type f -exec bash -c "sedi \"s/$OLD_NAME_SNAKE/$NEW_NAME_SNAKE/g\" \"{}\"" \; + +echo "...Done." From 70e536a1c2d9a467b3a7e03b001ae29504df69e2 Mon Sep 17 00:00:00 2001 From: Rocket Date: Wed, 17 Apr 2024 18:05:03 -0700 Subject: [PATCH 33/76] *Update rename-template-app.sh --- template-only-bin/rename-template-app.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template-only-bin/rename-template-app.sh b/template-only-bin/rename-template-app.sh index b3df10f..cbd3c3c 100755 --- a/template-only-bin/rename-template-app.sh +++ b/template-only-bin/rename-template-app.sh @@ -37,11 +37,11 @@ if [ -d "$OLD_NAME" ]; then fi # Rename all kebab-case instances -echo "Performing a find-and-replace for all instances of '$OLD_NAME_KEBAB' with '$NEW_NAME_KEBAB'..." +echo "Performing a find-and-replace for all instances of (kebab-case) '$OLD_NAME_KEBAB' with '$NEW_NAME_KEBAB'..." LC_ALL=C find . -type f -exec bash -c "sedi \"s/$OLD_NAME_KEBAB/$NEW_NAME_KEBAB/g\" \"{}\"" \; # Rename all snake-case instances -echo "Performing a find-and-replace for all instances of '$OLD_NAME_SNAKE' with '$NEW_NAME_SNAKE'..." +echo "Performing a find-and-replace for all instances of (snake-case) '$OLD_NAME_SNAKE' with '$NEW_NAME_SNAKE'..." LC_ALL=C find . -type f -exec bash -c "sedi \"s/$OLD_NAME_SNAKE/$NEW_NAME_SNAKE/g\" \"{}\"" \; echo "...Done." From f7fb59062c1b588c65256c035fab1188d1ab16f3 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 13:54:35 -0700 Subject: [PATCH 34/76] *Use bin/rails over bundle exec --- app-rails/Makefile | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app-rails/Makefile b/app-rails/Makefile index 4a46a5d..45afaa0 100644 --- a/app-rails/Makefile +++ b/app-rails/Makefile @@ -37,15 +37,9 @@ endif # You can set this by either running `export RAILS_RUN_APPROACH=local` in your shell or add # it to your ~/.zshrc file (and run `source ~/.zshrc`) ifeq "$(RAILS_RUN_APPROACH)" "local" -BUNDLE_EXEC_CMD := bundle exec +RAILS_RUN_CMD := bin/rails else -BUNDLE_EXEC_CMD := $(DOCKER_CMD) compose run $(DOCKER_EXEC_ARGS) --rm $(APP_NAME) bundle exec -endif - -ifeq "$(RAILS_RUN_APPROACH)" "local" -RAILS_RUN_CMD := bundle exec rails -else -RAILS_RUN_CMD := $(DOCKER_CMD) compose run $(DOCKER_EXEC_ARGS) --rm $(APP_NAME) bundle exec rails +RAILS_RUN_CMD := $(DOCKER_CMD) compose run $(DOCKER_EXEC_ARGS) --rm $(APP_NAME) bin/rails endif ifeq "$(RAILS_RUN_APPROACH)" "local" From a25f7e3af15c5145d2637a555ba72e91f6c3f23e Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 13:57:19 -0700 Subject: [PATCH 35/76] *Prefix AWS environment variables --- .../app/adapters/auth/cognito_adapter.rb | 26 +++++++++---------- app-rails/config/environments/development.rb | 2 +- app-rails/config/storage.yml | 2 +- app-rails/local.env.example | 8 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app-rails/app/adapters/auth/cognito_adapter.rb b/app-rails/app/adapters/auth/cognito_adapter.rb index 9ddad83..97de4bb 100644 --- a/app-rails/app/adapters/auth/cognito_adapter.rb +++ b/app-rails/app/adapters/auth/cognito_adapter.rb @@ -14,7 +14,7 @@ def initialize(client: Aws::CognitoIdentityProvider::Client.new) def create_account(email, password) begin response = @client.sign_up( - client_id: ENV["COGNITO_CLIENT_ID"], + client_id: ENV["AWS_COGNITO_CLIENT_ID"], secret_hash: get_secret_hash(email), username: email, password: password, @@ -47,7 +47,7 @@ def create_account(email, password) def forgot_password(email) begin response = @client.forgot_password( - client_id: ENV["COGNITO_CLIENT_ID"], + client_id: ENV["AWS_COGNITO_CLIENT_ID"], secret_hash: get_secret_hash(email), username: email ) @@ -64,7 +64,7 @@ def forgot_password(email) def confirm_forgot_password(email, code, password) begin @client.confirm_forgot_password( - client_id: ENV["COGNITO_CLIENT_ID"], + client_id: ENV["AWS_COGNITO_CLIENT_ID"], secret_hash: get_secret_hash(email), username: email, confirmation_code: code, @@ -84,7 +84,7 @@ def confirm_forgot_password(email, code, password) def change_email(uid, new_email) begin response = @client.admin_update_user_attributes( - user_pool_id: ENV["COGNITO_USER_POOL_ID"], + user_pool_id: ENV["AWS_COGNITO_USER_POOL_ID"], username: uid, user_attributes: [ { @@ -114,8 +114,8 @@ def change_email(uid, new_email) def initiate_auth(email, password) begin response = @client.admin_initiate_auth( - user_pool_id: ENV["COGNITO_USER_POOL_ID"], - client_id: ENV["COGNITO_CLIENT_ID"], + user_pool_id: ENV["AWS_COGNITO_USER_POOL_ID"], + client_id: ENV["AWS_COGNITO_CLIENT_ID"], auth_flow: "ADMIN_USER_PASSWORD_AUTH", auth_parameters: { "USERNAME" => email, @@ -151,7 +151,7 @@ def initiate_auth(email, password) def resend_verification_code(email) begin @client.resend_confirmation_code( - client_id: ENV["COGNITO_CLIENT_ID"], + client_id: ENV["AWS_COGNITO_CLIENT_ID"], secret_hash: get_secret_hash(email), username: email ) @@ -163,8 +163,8 @@ def resend_verification_code(email) def respond_to_auth_challenge(code, challenge = {}) begin response = @client.admin_respond_to_auth_challenge( - client_id: ENV["COGNITO_CLIENT_ID"], - user_pool_id: ENV["COGNITO_USER_POOL_ID"], + client_id: ENV["AWS_COGNITO_CLIENT_ID"], + user_pool_id: ENV["AWS_COGNITO_USER_POOL_ID"], challenge_name: "SOFTWARE_TOKEN_MFA", session: challenge[:session], challenge_responses: { @@ -184,7 +184,7 @@ def respond_to_auth_challenge(code, challenge = {}) def verify_account(email, code) begin @client.confirm_sign_up( - client_id: ENV["COGNITO_CLIENT_ID"], + client_id: ENV["AWS_COGNITO_CLIENT_ID"], secret_hash: get_secret_hash(email), username: email, confirmation_code: code @@ -242,7 +242,7 @@ def verify_software_token(code, access_token) def disable_software_token(uid) begin @client.admin_set_user_mfa_preference( - user_pool_id: ENV["COGNITO_USER_POOL_ID"], + user_pool_id: ENV["AWS_COGNITO_USER_POOL_ID"], username: uid, software_token_mfa_settings: { enabled: false, @@ -265,8 +265,8 @@ def get_auth_result(response) end def get_secret_hash(username) - message = username + ENV["COGNITO_CLIENT_ID"] - key = ENV["COGNITO_CLIENT_SECRET"] + message = username + ENV["AWS_COGNITO_CLIENT_ID"] + key = ENV["AWS_COGNITO_CLIENT_SECRET"] Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", key, message)) end diff --git a/app-rails/config/environments/development.rb b/app-rails/config/environments/development.rb index 41bc7bd..352e112 100644 --- a/app-rails/config/environments/development.rb +++ b/app-rails/config/environments/development.rb @@ -36,7 +36,7 @@ config.cache_store = :null_store end - config.active_storage.service = if ENV["BUCKET_NAME"] then :amazon else :local end + config.active_storage.service = if ENV["AWS_BUCKET_NAME"] then :amazon else :local end config.action_mailer.delivery_method = if ENV["SES_EMAIL"] then :sesv2 else :letter_opener end diff --git a/app-rails/config/storage.yml b/app-rails/config/storage.yml index 04f68c1..2ff2e69 100644 --- a/app-rails/config/storage.yml +++ b/app-rails/config/storage.yml @@ -8,7 +8,7 @@ local: amazon: service: S3 - bucket: <%= ENV.fetch("BUCKET_NAME") { nil } %> + bucket: <%= ENV.fetch("AWS_BUCKET_NAME") { nil } %> # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) diff --git a/app-rails/local.env.example b/app-rails/local.env.example index ee8f7bb..7a03a83 100644 --- a/app-rails/local.env.example +++ b/app-rails/local.env.example @@ -18,15 +18,15 @@ APP_PORT=3000 AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION= -BUCKET_NAME= +AWS_BUCKET_NAME= ############################ # Auth ############################ -COGNITO_CLIENT_SECRET= -COGNITO_USER_POOL_ID= -COGNITO_CLIENT_ID= +AWS_COGNITO_CLIENT_SECRET= +AWS_COGNITO_USER_POOL_ID= +AWS_COGNITO_CLIENT_ID= ############################ # Database From b2c8ed7e9e0b00c8802223b45a4af7f4e01ef62f Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 13:59:20 -0700 Subject: [PATCH 36/76] *Use ternary expression in development.rb --- app-rails/config/environments/development.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-rails/config/environments/development.rb b/app-rails/config/environments/development.rb index 352e112..abaf811 100644 --- a/app-rails/config/environments/development.rb +++ b/app-rails/config/environments/development.rb @@ -36,9 +36,9 @@ config.cache_store = :null_store end - config.active_storage.service = if ENV["AWS_BUCKET_NAME"] then :amazon else :local end + config.active_storage.service = ENV["AWS_BUCKET_NAME"] ? :amazon : :local - config.action_mailer.delivery_method = if ENV["SES_EMAIL"] then :sesv2 else :letter_opener end + config.action_mailer.delivery_method = ENV["SES_EMAIL"] ? :sesv2 : :letter_opener # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false From 75802175a61e5879c44aa638fe0df5e0d6ac2021 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 14:29:15 -0700 Subject: [PATCH 37/76] Remove init-postgres for project with one database --- docker-compose.yml | 1 - init-postgres.sql | 6 ------ template-only-bin/install-template.sh | 1 - 3 files changed, 8 deletions(-) delete mode 100644 init-postgres.sql diff --git a/docker-compose.yml b/docker-compose.yml index e69cb01..7f6afa2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,6 @@ services: - "5432:5432" volumes: - database_data:/var/lib/postgresql/data - - ./init-postgres.sql:/docker-entrypoint-initdb.d/init-postgres.sql # Rails app # Configured for "development" RAILS_ENV diff --git a/init-postgres.sql b/init-postgres.sql deleted file mode 100644 index 1f1d5bd..0000000 --- a/init-postgres.sql +++ /dev/null @@ -1,6 +0,0 @@ --- This is run when you start the Docker postgres container. --- Create separate databases for each application. --- This only runs if the postgresql data directory is empty. --- See the "Initialization scripts" section of https://hub.docker.com/_/postgres - -CREATE DATABASE app_rails; \ No newline at end of file diff --git a/template-only-bin/install-template.sh b/template-only-bin/install-template.sh index 1ec5964..5721f46 100755 --- a/template-only-bin/install-template.sh +++ b/template-only-bin/install-template.sh @@ -26,7 +26,6 @@ cp -r \ .gitignore \ $TEMPLATE_SHORT_NAME \ docker-compose.yml \ - init-postgres.sql \ docs \ $CUR_DIR cd - >& /dev/null From 4f5c813b798bcac28ed3e8ec466aa0f7c23a08a6 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 14:29:28 -0700 Subject: [PATCH 38/76] Fix docker service name and bind mount --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7f6afa2..7d3deba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: # Rails app # Configured for "development" RAILS_ENV - app_rails: + app-rails: build: context: ./app-rails target: dev @@ -31,7 +31,7 @@ services: ports: - 3100:3000 volumes: - - ./app:/rails + - ./app-rails:/rails # Use named volumes for directories that the container should use the guest # machine's dir instead of the host machine's dir, which may be divergent. # This is especially true for any dependency or temp directories. From c00e3a03dd6c3639fbdc0adb4b1b569f84684552 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 15:09:27 -0700 Subject: [PATCH 39/76] Fix dev docker setup --- app-rails/Gemfile | 4 ++++ app-rails/Gemfile.lock | 4 ++++ app-rails/package.json | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app-rails/Gemfile b/app-rails/Gemfile index fc07ca1..e7a8c73 100644 --- a/app-rails/Gemfile +++ b/app-rails/Gemfile @@ -85,9 +85,13 @@ group :development, :test do end group :development do + # Linting gem "rubocop-rails-omakase", require: false gem "rubocop-rspec", require: false + # Sass compiling + gem "sassc", "~> 2.4" + # Test notifications locally without sending the emails gem "letter_opener" diff --git a/app-rails/Gemfile.lock b/app-rails/Gemfile.lock index ac9e5db..256dc02 100644 --- a/app-rails/Gemfile.lock +++ b/app-rails/Gemfile.lock @@ -181,6 +181,7 @@ GEM faraday-net_http (>= 2.0, < 3.2) faraday-net_http (3.1.0) net-http + ffi (1.16.3) globalid (1.2.1) activesupport (>= 6.1) i18n (1.14.4) @@ -387,6 +388,8 @@ GEM rexml ruby-progressbar (1.13.0) rubyzip (2.3.2) + sassc (2.4.0) + ffi (~> 1.9) selenium-webdriver (4.19.0) base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) @@ -480,6 +483,7 @@ DEPENDENCIES rspec-rails (~> 6.1.0) rubocop-rails-omakase rubocop-rspec + sassc (~> 2.4) selenium-webdriver simplecov sprockets-rails diff --git a/app-rails/package.json b/app-rails/package.json index c9528af..6e4173d 100644 --- a/app-rails/package.json +++ b/app-rails/package.json @@ -3,7 +3,7 @@ "private": true, "version": "1.0.0", "scripts": { - "build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css ./app/assets/stylesheets/admin.scss:./app/assets/builds/admin.css --load-path=node_modules --load-path=node_modules/@uswds/uswds/packages" + "build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css --load-path=node_modules --load-path=node_modules/@uswds/uswds/packages" }, "dependencies": { "@uswds/uswds": "^3.7.1" From 8ed9ef25e893c7ff97858c508a4f57060b855d7c Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 15:10:09 -0700 Subject: [PATCH 40/76] *Add Makefile support for multiple docker compose files --- app-rails/Makefile | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app-rails/Makefile b/app-rails/Makefile index 45afaa0..973b1da 100644 --- a/app-rails/Makefile +++ b/app-rails/Makefile @@ -32,6 +32,9 @@ else DOCKER_CMD := docker endif +# Support executing commands in an existing container +DOCKER_APPROACH := run + # By default, all rails commands will run inside of the docker container # if you wish to run this natively, add RAILS_RUN_APPROACH=local to your environment vars # You can set this by either running `export RAILS_RUN_APPROACH=local` in your shell or add @@ -39,13 +42,13 @@ endif ifeq "$(RAILS_RUN_APPROACH)" "local" RAILS_RUN_CMD := bin/rails else -RAILS_RUN_CMD := $(DOCKER_CMD) compose run $(DOCKER_EXEC_ARGS) --rm $(APP_NAME) bin/rails +RAILS_RUN_CMD := $(DOCKER_CMD) compose $(DOCKER_COMPOSE_ARGS) $(DOCKER_APPROACH) $(DOCKER_RUN_ARGS) --rm $(APP_NAME) bin/rails endif ifeq "$(RAILS_RUN_APPROACH)" "local" RUBY_RUN_CMD := else -RUBY_RUN_CMD := $(DOCKER_CMD) compose run $(DOCKER_EXEC_ARGS) --rm $(APP_NAME) +RUBY_RUN_CMD := $(DOCKER_CMD) compose $(DOCKER_COMPOSE_ARGS) $(DOCKER_APPROACH) $(DOCKER_RUN_ARGS) --rm $(APP_NAME) endif # Docker user configuration @@ -99,7 +102,7 @@ clean-native: ## Remove native installs git checkout vendor/bundle/.keep clean-container: ## Remove just the container related volumes - $(DOCKER_CMD) compose down --volumes $(APP_NAME) + $(DOCKER_CMD) compose $(DOCKER_COMPOSE_ARGS) down --volumes $(APP_NAME) ################################################## # Build & Run @@ -114,27 +117,27 @@ release-build: . build: ## Build the Docker container - $(DOCKER_CMD) compose build $(APP_NAME) + $(DOCKER_CMD) compose $(DOCKER_COMPOSE_ARGS) build $(APP_NAME) clean-volumes: ## Remove container volumes (which includes the DB state) - $(DOCKER_CMD) compose down --volumes + $(DOCKER_CMD) compose $(DOCKER_COMPOSE_ARGS) down --volumes start-native: ## Run Rails natively, outside Docker start-native: db-up ./bin/dev start-container: ## Run within Docker - $(DOCKER_CMD) compose up $(APP_NAME) + $(DOCKER_CMD) compose $(DOCKER_COMPOSE_ARGS) up $(APP_NAME) stop-containers: ## Stop Docker - $(DOCKER_CMD) compose down + $(DOCKER_CMD) compose $(DOCKER_COMPOSE_ARGS) down ################################################## # Database ################################################## db-up: ## Run just the database container - $(DOCKER_CMD) compose up --remove-orphans --detach database + $(DOCKER_CMD) compose $(DOCKER_COMPOSE_ARGS) up --remove-orphans --detach database db-migrate: ## Run database migrations $(RAILS_RUN_CMD) db:migrate From a68a5091929a727cbcfb81c4376492fbdc578b24 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 15:10:18 -0700 Subject: [PATCH 41/76] Troubleshoot release docker build --- app-rails/Dockerfile | 8 ++++++++ docker-compose.mock-release.yml | 29 +++++++++++++++++++++++++++ template-only-bin/install-template.sh | 1 + 3 files changed, 38 insertions(+) create mode 100644 docker-compose.mock-release.yml diff --git a/app-rails/Dockerfile b/app-rails/Dockerfile index 7623d77..5fe1152 100644 --- a/app-rails/Dockerfile +++ b/app-rails/Dockerfile @@ -72,6 +72,9 @@ RUN bundle config set --local without development test && \ bundle install && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git +# Copy application code +COPY . . + # Precompile bootsnap code for faster boot times RUN bundle exec bootsnap precompile --gemfile app/ lib/ @@ -84,6 +87,11 @@ RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile ########################################################################################## FROM base as release +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" + # Install packages needed for deployment RUN apt-get update -qq && \ apt-get install -y --no-install-recommends unzip python3-venv python-is-python3 curl libvips postgresql-client && \ diff --git a/docker-compose.mock-release.yml b/docker-compose.mock-release.yml new file mode 100644 index 0000000..7ff14ca --- /dev/null +++ b/docker-compose.mock-release.yml @@ -0,0 +1,29 @@ +services: + database: + image: postgres:14-alpine + command: postgres -c "log_lock_waits=on" -N 1000 -c "fsync=off" + environment: + POSTGRES_PASSWORD: secret123 + POSTGRES_USER: app_rails + healthcheck: + test: "pg_isready --username=app_rails" + timeout: 10s + retries: 20 + ports: + - "5432:5432" + + # Rails app + # Configured for "production" RAILS_ENV + app-rails: + build: + context: ./app-rails + target: release + depends_on: + database: + condition: service_healthy + env_file: ./app-rails/.env + environment: + - DB_HOST=database + - RAILS_BINDING=0.0.0.0 + ports: + - 3200:3000 diff --git a/template-only-bin/install-template.sh b/template-only-bin/install-template.sh index 5721f46..596f401 100755 --- a/template-only-bin/install-template.sh +++ b/template-only-bin/install-template.sh @@ -26,6 +26,7 @@ cp -r \ .gitignore \ $TEMPLATE_SHORT_NAME \ docker-compose.yml \ + docker-compose.mock-release.yml \ docs \ $CUR_DIR cd - >& /dev/null From 6e0311813892d0121a3e841647ddbd0e22ada0f8 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 15:48:45 -0700 Subject: [PATCH 42/76] *Configure setup for mock-production --- app-rails/config/database.yml | 5 + .../config/environments/mock-production.rb | 103 ++++++++++++++++++ ....yml => docker-compose.mock-production.yml | 6 + template-only-bin/install-template.sh | 2 +- 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 app-rails/config/environments/mock-production.rb rename docker-compose.mock-release.yml => docker-compose.mock-production.yml (72%) diff --git a/app-rails/config/database.yml b/app-rails/config/database.yml index 49a5ba1..868317a 100644 --- a/app-rails/config/database.yml +++ b/app-rails/config/database.yml @@ -64,6 +64,11 @@ test: <<: *default database: app_rails_test +# For mock-production: To test a "production"-like environment on a local machine, +# allow the use of a non-RDS database. +mock-production: + <<: *default + # 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. diff --git a/app-rails/config/environments/mock-production.rb b/app-rails/config/environments/mock-production.rb new file mode 100644 index 0000000..ecd6917 --- /dev/null +++ b/app-rails/config/environments/mock-production.rb @@ -0,0 +1,103 @@ +require "active_support/core_ext/integer/time" + +# Custom setting: set the default url. +Rails.application.default_url_options = { host: ENV["APP_HOST"], port: ENV["APP_PORT"] } + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fall back to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :amazon + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # For mock-production: disable assume SSL to be able to test on a local machine + config.assume_ssl = false + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # For mock-production: disable SSL to be able to test on a local machine + config.force_ssl = false + + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new(STDOUT) + .tap { |logger| logger.formatter = ::Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "app_rails_production" + + config.action_mailer.delivery_method = :sesv2 + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/docker-compose.mock-release.yml b/docker-compose.mock-production.yml similarity index 72% rename from docker-compose.mock-release.yml rename to docker-compose.mock-production.yml index 7ff14ca..0555d75 100644 --- a/docker-compose.mock-release.yml +++ b/docker-compose.mock-production.yml @@ -25,5 +25,11 @@ services: environment: - DB_HOST=database - RAILS_BINDING=0.0.0.0 + # The following env vars allow testing a "production"-like environment on a local + # machine. + - RAILS_ENV=mock-production + - SECRET_KEY_BASE=verysecret + - DISABLE_DATABASE_ENVIRONMENT_CHECK=1 + - AWS_DEFAULT_REGION=us-east-1 ports: - 3200:3000 diff --git a/template-only-bin/install-template.sh b/template-only-bin/install-template.sh index 596f401..9ea7f40 100755 --- a/template-only-bin/install-template.sh +++ b/template-only-bin/install-template.sh @@ -26,7 +26,7 @@ cp -r \ .gitignore \ $TEMPLATE_SHORT_NAME \ docker-compose.yml \ - docker-compose.mock-release.yml \ + docker-compose.mock-production.yml \ docs \ $CUR_DIR cd - >& /dev/null From ee3a6eb013a1c9e3f51e2d07dcb3636a0ea30218 Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 16:10:19 -0700 Subject: [PATCH 43/76] Genericize i18n strings --- .../config/locales/views/application/en.yml | 16 ++-------------- .../config/locales/views/application/es-US.yml | 1 - app-rails/config/locales/views/home/en.yml | 8 ++++---- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/app-rails/config/locales/views/application/en.yml b/app-rails/config/locales/views/application/en.yml index 80db087..ace9f59 100644 --- a/app-rails/config/locales/views/application/en.yml +++ b/app-rails/config/locales/views/application/en.yml @@ -11,31 +11,19 @@ en: account: My account language_toggle: English logout: "Log out" - title: "Paid Family & Medical Leave" + title: "Application Title" close: "Close" flash: error_heading: zero: "An unexpected error occurred" one: "An error occurred" other: "%{count} errors occurred" - error_fallback: "Sorry, we encountered an unexpected error. If this continues to happen, you may call the Paid Family Leave Contact Center." + error_fallback: "Sorry, we encountered an unexpected error. If this continues to happen, you may call the Contact Center." reload_page: "Reload page" breadcrumbs: home: "Home" buttons: submit: "Submit" - requestable_step_indicator: - submitted: "Submitted" - in_review: "In review" - determination_made: "Determination made" - requestable_index: - existing_requests: - heading: "Existing requests" - col_created_at: "Date submitted" - col_status: "Status" - col_actions: "Actions" - action_view_rfi: "Respond to RFI" - action_none: "You have no pending actions at this time." tables: column: "Column" description: "Description" diff --git a/app-rails/config/locales/views/application/es-US.yml b/app-rails/config/locales/views/application/es-US.yml index c988031..84c85dc 100644 --- a/app-rails/config/locales/views/application/es-US.yml +++ b/app-rails/config/locales/views/application/es-US.yml @@ -2,4 +2,3 @@ es-US: header: language_toggle: EspaÃąol logout: Cerrar sesiÃģn - title: Permiso Familiar y MÃĐdico Pagado diff --git a/app-rails/config/locales/views/home/en.yml b/app-rails/config/locales/views/home/en.yml index 31f54b7..9c24a71 100644 --- a/app-rails/config/locales/views/home/en.yml +++ b/app-rails/config/locales/views/home/en.yml @@ -2,11 +2,11 @@ en: home: index: title: "Get started" - intro: "You can either apply for leave or manage claims for an organization. If you need to do both, you will need to create separate accounts." - claimant_heading: "Claimants" + intro: "You can either apply for benefits or manage applications for an organization. If you need to do both, you will need to create separate accounts." + claimant_heading: "Applicants" claimant_body: "I am applying for paid leave benefits." - claimant_signup: "Create a Claimant account" + claimant_signup: "Create an Applicant account" employer_heading: "Employers" - employer_body: "I manage claims for the employees in my organization." + employer_body: "I manage applications for my organization." employer_signup: "Create an Employer account" or_sign_in: "Or sign into an existing account" From 76e531b5ace6682ca9c40137395355e21a008cbe Mon Sep 17 00:00:00 2001 From: Rocket Date: Tue, 21 May 2024 16:36:30 -0700 Subject: [PATCH 44/76] Refactored claimant -> applicant --- .../images/{claimant.jpg => applicant.jpg} | Bin .../app/controllers/application_controller.rb | 1 - .../users/registrations_controller.rb | 4 ++-- app-rails/app/models/user.rb | 4 ++-- app-rails/app/models/user_role.rb | 2 +- app-rails/app/services/auth_service.rb | 2 +- app-rails/app/views/home/index.html.erb | 10 +++++----- .../views/users/registrations/new.html.erb | 14 +++++++------- .../app/views/users/sessions/new.html.erb | 2 +- app-rails/config/locales/views/home/en.yml | 6 +++--- app-rails/config/locales/views/users/en.yml | 10 +++++----- app-rails/config/routes.rb | 2 +- app-rails/local.env.example | 2 +- .../controllers/users/mfa_controller_spec.rb | 1 - .../users/registrations_controller_spec.rb | 10 +++++----- .../users/sessions_controller_spec.rb | 4 ++-- app-rails/spec/factories/user_role_factory.rb | 4 ++-- app-rails/spec/factories/users_factory.rb | 4 ++-- .../forms/users/registration_form_spec.rb | 2 +- app-rails/spec/models/user_spec.rb | 18 +++++++++--------- app-rails/spec/services/auth_service_spec.rb | 2 +- docs/app-rails/auth.md | 2 +- 22 files changed, 52 insertions(+), 54 deletions(-) rename app-rails/app/assets/images/{claimant.jpg => applicant.jpg} (100%) diff --git a/app-rails/app/assets/images/claimant.jpg b/app-rails/app/assets/images/applicant.jpg similarity index 100% rename from app-rails/app/assets/images/claimant.jpg rename to app-rails/app/assets/images/applicant.jpg diff --git a/app-rails/app/controllers/application_controller.rb b/app-rails/app/controllers/application_controller.rb index 2ac6cc7..9144124 100644 --- a/app-rails/app/controllers/application_controller.rb +++ b/app-rails/app/controllers/application_controller.rb @@ -28,7 +28,6 @@ def after_sign_in_path_for(resource) return employers_path end - # @TODO: Route to a claimant landing page users_account_path end end diff --git a/app-rails/app/controllers/users/registrations_controller.rb b/app-rails/app/controllers/users/registrations_controller.rb index 63fa7f1..03746ab 100644 --- a/app-rails/app/controllers/users/registrations_controller.rb +++ b/app-rails/app/controllers/users/registrations_controller.rb @@ -4,8 +4,8 @@ class Users::RegistrationsController < ApplicationController layout "users" skip_after_action :verify_authorized - def new_claimant - @form = Users::RegistrationForm.new(role: "claimant") + def new_applicant + @form = Users::RegistrationForm.new(role: "applicant") render :new end diff --git a/app-rails/app/models/user.rb b/app-rails/app/models/user.rb index 7737520..4b5b014 100644 --- a/app-rails/app/models/user.rb +++ b/app-rails/app/models/user.rb @@ -14,8 +14,8 @@ class User < ApplicationRecord validates :provider, presence: true # == Methods ============================================================== - def claimant? - user_role&.claimant? + def applicant? + user_role&.applicant? end def employer? diff --git a/app-rails/app/models/user_role.rb b/app-rails/app/models/user_role.rb index f4b2c7b..0b1b8d8 100644 --- a/app-rails/app/models/user_role.rb +++ b/app-rails/app/models/user_role.rb @@ -1,7 +1,7 @@ class UserRole < ApplicationRecord # == Enums ================================================================ # See `technical-foundation.md#enums` for important note about enums. - enum :role, { claimant: 0, employer: 1 }, default: :claimant, validate: true + enum :role, { applicant: 0, employer: 1 }, default: :applicant, validate: true # == Relationships ======================================================== belongs_to :user diff --git a/app-rails/app/services/auth_service.rb b/app-rails/app/services/auth_service.rb index 9bcab30..d5e70c4 100644 --- a/app-rails/app/services/auth_service.rb +++ b/app-rails/app/services/auth_service.rb @@ -71,7 +71,7 @@ def disable_software_token(user) private - def create_db_user(uid, email, provider, role = "claimant") + def create_db_user(uid, email, provider, role = "applicant") Rails.logger.info "Creating User uid: #{uid}, and UserRole: #{role}" user = User.create!( diff --git a/app-rails/app/views/home/index.html.erb b/app-rails/app/views/home/index.html.erb index 3094976..64ed7c7 100644 --- a/app-rails/app/views/home/index.html.erb +++ b/app-rails/app/views/home/index.html.erb @@ -14,22 +14,22 @@

- <%= t('.claimant_heading') %> + <%= t('.applicant_heading') %>

- <%= image_tag 'claimant.jpg', alt: "Man with two younger children" %> + <%= image_tag 'applicant.jpg', alt: "Man with two younger children" %>

- <%= t('.claimant_body') %> + <%= t('.applicant_body') %>