mirror of
https://github.com/System-End/identity-vault.git
synced 2026-04-19 15:18:23 +00:00
initial public commit!!!
This commit is contained in:
commit
a260c265f0
326 changed files with 15473 additions and 0 deletions
47
.dockerignore
Normal file
47
.dockerignore
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.
|
||||
|
||||
# Ignore git directory.
|
||||
/.git/
|
||||
/.gitignore
|
||||
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
|
||||
# Ignore all environment files.
|
||||
/.env*
|
||||
|
||||
# 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
|
||||
|
||||
# Ignore CI service files.
|
||||
/.github
|
||||
|
||||
# Ignore development files
|
||||
/.devcontainer
|
||||
|
||||
# Ignore Docker-related files
|
||||
/.dockerignore
|
||||
/Dockerfile*
|
||||
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
|
|
@ -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
|
||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: bundler
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
39
.github/workflows/ci.yml
vendored
Normal file
39
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
scan_ruby:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: .ruby-version
|
||||
bundler-cache: true
|
||||
|
||||
- name: Scan for common Rails security vulnerabilities using static analysis
|
||||
run: bin/brakeman --no-pager
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: .ruby-version
|
||||
bundler-cache: true
|
||||
|
||||
- name: Lint code for consistent style
|
||||
run: bin/rubocop -f github
|
||||
|
||||
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||
#
|
||||
# Temporary files generated by your text editor or operating system
|
||||
# belong in git's global ignore instead:
|
||||
# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore`
|
||||
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
|
||||
# Ignore all environment files.
|
||||
/.env*
|
||||
|
||||
# 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
|
||||
|
||||
# Vite Ruby
|
||||
/public/vite*
|
||||
node_modules
|
||||
# Vite uses dotenv and suggests to ignore local-only env files. See
|
||||
# https://vitejs.dev/guide/env-and-mode.html#env-files
|
||||
*.local
|
||||
|
||||
|
||||
/config/credentials/production.key
|
||||
|
||||
/config/credentials/staging.key
|
||||
|
||||
/config/credentials/development.key
|
||||
8
.rubocop.yml
Normal file
8
.rubocop.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Omakase Ruby styling for Rails
|
||||
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
|
||||
|
||||
# Overwrite or add rules to create your own house style
|
||||
#
|
||||
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
|
||||
# Layout/SpaceInsideArrayLiteralBrackets:
|
||||
# Enabled: false
|
||||
1
.ruby-version
Normal file
1
.ruby-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.4.4
|
||||
98
Dockerfile
Normal file
98
Dockerfile
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# syntax=docker/dockerfile:1.4
|
||||
# check=error=true
|
||||
|
||||
# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
|
||||
# docker build -t identity_vault .
|
||||
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name identity_vault identity_vault
|
||||
|
||||
# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
|
||||
|
||||
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
|
||||
ARG RUBY_VERSION=3.4.4
|
||||
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
|
||||
|
||||
# Rails app lives here
|
||||
WORKDIR /rails
|
||||
|
||||
# Install base packages with runtime libraries for libheif
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y curl libjemalloc2 libvips imagemagick postgresql-client libffi-dev \
|
||||
libjpeg62-turbo libaom3 libx265-199 libde265-0 libpng16-16 wget && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG NODE_VERSION=23.6.0
|
||||
ARG YARN_VERSION=1.22.22
|
||||
ENV PATH=/usr/local/node/bin:$PATH
|
||||
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
|
||||
/tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
|
||||
npm install -g yarn@$YARN_VERSION && \
|
||||
rm -rf /tmp/node-build-master
|
||||
|
||||
ENV BUNDLE_DEPLOYMENT="1" \
|
||||
BUNDLE_PATH="/usr/local/bundle" \
|
||||
BUNDLE_WITHOUT="development" \
|
||||
LD_LIBRARY_PATH="/usr/local/lib"
|
||||
|
||||
# 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 curl libjemalloc2 libvips imagemagick postgresql-client libffi-dev build-essential git libpq-dev libyaml-dev pkg-config \
|
||||
cmake libjpeg-dev libpng-dev libaom-dev libx265-dev libde265-dev && \
|
||||
# Build libheif from latest source with examples
|
||||
cd /tmp && \
|
||||
git clone --depth 1 https://github.com/strukturag/libheif.git && \
|
||||
cd libheif && \
|
||||
mkdir build && cd build && \
|
||||
cmake --preset=release -DWITH_EXAMPLES=ON -DENABLE_PLUGIN_LOADING=NO .. && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
ldconfig && \
|
||||
# Clean up
|
||||
cd / && rm -rf /tmp/libheif && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 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 package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Precompile bootsnap code for faster boot times
|
||||
RUN bundle exec bootsnap precompile app/ lib/ && \
|
||||
RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile && \
|
||||
rm -rf node_modules
|
||||
|
||||
# Final stage for app image
|
||||
FROM base
|
||||
|
||||
# Copy built artifacts: gems, application
|
||||
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
|
||||
COPY --from=build /rails /rails
|
||||
# Copy libheif libraries and examples from build stage
|
||||
COPY --from=build /usr/local/lib/libheif* /usr/local/lib/
|
||||
COPY --from=build /usr/local/bin/heif-* /usr/local/bin/
|
||||
COPY --from=build /usr/local/include/libheif /usr/local/include/libheif
|
||||
RUN ldconfig
|
||||
|
||||
# Run and own only the runtime files as a non-root users for security
|
||||
RUN groupadd --system --gid 1000 rails && \
|
||||
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
||||
chown -R rails:rails db log storage tmp
|
||||
USER 1000:1000
|
||||
|
||||
# Entrypoint prepares the database.
|
||||
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
|
||||
|
||||
# Start server via Thruster by default, this can be overwritten at runtime
|
||||
EXPOSE 80
|
||||
CMD ["./bin/thrust", "./bin/rails", "server"]
|
||||
61
Dockerfile.worker
Normal file
61
Dockerfile.worker
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# check=error=true
|
||||
|
||||
# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
|
||||
# docker build -t identity_vault .
|
||||
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name identity_vault identity_vault
|
||||
|
||||
# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
|
||||
|
||||
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
|
||||
ARG RUBY_VERSION=3.4.4
|
||||
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
|
||||
|
||||
# Rails app lives here
|
||||
WORKDIR /rails
|
||||
|
||||
# Install base packages
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y curl libjemalloc2 libvips imagemagick libheif-dev postgresql-client libffi-dev && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV 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 curl libjemalloc2 libvips imagemagick libheif-dev postgresql-client libffi-dev build-essential git libpq-dev libyaml-dev pkg-config && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 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/
|
||||
|
||||
# Final stage for app image
|
||||
FROM base
|
||||
|
||||
# Copy built artifacts: gems, application
|
||||
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
|
||||
COPY --from=build /rails /rails
|
||||
|
||||
# Run and own only the runtime files as a non-root users for security
|
||||
RUN groupadd --system --gid 1000 rails && \
|
||||
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
||||
chown -R rails:rails db log storage tmp
|
||||
USER 1000:1000
|
||||
|
||||
CMD ["bundle", "exec", "good_job", "start"]
|
||||
123
Gemfile
Normal file
123
Gemfile
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
|
||||
gem "rails", "~> 8.0.2"
|
||||
# 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 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
|
||||
|
||||
# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
|
||||
gem "thruster", require: false
|
||||
|
||||
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
|
||||
gem "image_processing", "~> 1.2"
|
||||
|
||||
group :development, :test do
|
||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
|
||||
|
||||
# Static analysis for security vulnerabilities [https://brakemanscanner.org/]
|
||||
gem "brakeman", require: false
|
||||
|
||||
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
|
||||
gem "rubocop-rails-omakase", require: false
|
||||
end
|
||||
|
||||
group :development do
|
||||
# Use console on exceptions pages [https://github.com/rails/web-console]
|
||||
gem "web-console"
|
||||
end
|
||||
|
||||
gem "dotenv", groups: [ :development, :test ]
|
||||
|
||||
gem "vite_rails"
|
||||
|
||||
gem "pundit", "~> 2.5"
|
||||
|
||||
gem "honeybadger", "~> 5.28"
|
||||
|
||||
gem "http", "~> 5.2"
|
||||
|
||||
gem "superform", "~> 0.5.1"
|
||||
|
||||
gem "phlex", "~> 2.2"
|
||||
|
||||
gem "phlex-rails", "~> 2.2"
|
||||
|
||||
gem "literal", "~> 1.7"
|
||||
|
||||
gem "jb", "~> 0.8.2"
|
||||
|
||||
gem "wicked", "~> 2.0"
|
||||
|
||||
gem "countries", "~> 7.1"
|
||||
|
||||
gem "awesome_print", "~> 1.9"
|
||||
|
||||
gem "active_storage_encryption", "~> 0.3.0"
|
||||
|
||||
gem "doorkeeper", "~> 5.8"
|
||||
|
||||
gem "aasm", "~> 5.5"
|
||||
|
||||
gem "kaminari", "~> 1.2"
|
||||
|
||||
gem "blind_index", "~> 2.7"
|
||||
|
||||
gem "lockbox", "~> 2.0"
|
||||
|
||||
gem "hashid-rails", "~> 1.4"
|
||||
|
||||
gem "public_activity", "~> 3.0"
|
||||
|
||||
gem "paper_trail", "~> 16.0"
|
||||
|
||||
gem "good_job", "~> 4.10"
|
||||
|
||||
group :development do
|
||||
gem "letter_opener_web", "~> 3.0"
|
||||
end
|
||||
|
||||
gem "aws-sdk-s3", "~> 1.189"
|
||||
|
||||
gem "lz_string", "~> 0.3.0"
|
||||
|
||||
gem "valid_email2", "~> 7.0"
|
||||
|
||||
gem "rails_semantic_logger", "~> 4.17"
|
||||
|
||||
gem "acts_as_paranoid", "~> 0.10.3"
|
||||
|
||||
gem "console1984", "~> 0.2.2"
|
||||
|
||||
gem "audits1984", "~> 0.1.7"
|
||||
|
||||
gem "propshaft", "~> 1.1"
|
||||
|
||||
gem "mini-levenshtein", "~> 0.1.2"
|
||||
|
||||
gem "faraday", "~> 2.13"
|
||||
|
||||
gem "ruby-vips", "~> 2.2"
|
||||
|
||||
gem "slack-ruby-client", "~> 2.6"
|
||||
|
||||
gem "redcarpet", "~> 3.6"
|
||||
|
||||
gem "flipper", "~> 1.3"
|
||||
gem "flipper-ui", "~> 1.3"
|
||||
gem "flipper-active_record", "~> 1.3"
|
||||
|
||||
gem "annotaterb", "~> 4.19"
|
||||
|
||||
gem "erb_lint", "~> 0.9.0", group: :development
|
||||
590
Gemfile.lock
Normal file
590
Gemfile.lock
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
aasm (5.5.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
actioncable (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activestorage (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
actionview (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.2)
|
||||
actionview (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activestorage (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
active_storage_encryption (0.3.0)
|
||||
activestorage
|
||||
block_cipher_kit (>= 0.0.4)
|
||||
rails (>= 7.2.2.1)
|
||||
serve_byte_range (~> 1.0)
|
||||
activejob (8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
activerecord (8.0.2)
|
||||
activemodel (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
acts_as_paranoid (0.10.3)
|
||||
activerecord (>= 6.1, < 8.1)
|
||||
activesupport (>= 6.1, < 8.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotaterb (4.19.0)
|
||||
activerecord (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
argon2-kdf (0.3.1)
|
||||
fiddle
|
||||
ast (2.4.3)
|
||||
audits1984 (0.1.7)
|
||||
console1984
|
||||
rinku
|
||||
rouge
|
||||
turbo-rails
|
||||
awesome_print (1.9.2)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1110.0)
|
||||
aws-sdk-core (3.225.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.102.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.189.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.2.0)
|
||||
benchmark (0.4.0)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
ast (~> 2.0)
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.1.9)
|
||||
bindex (0.8.1)
|
||||
blind_index (2.7.0)
|
||||
activesupport (>= 7.1)
|
||||
argon2-kdf (>= 0.2)
|
||||
block_cipher_kit (0.0.4)
|
||||
bootsnap (1.18.6)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.1.0)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
childprocess (5.1.0)
|
||||
logger (~> 1.5)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
console1984 (0.2.2)
|
||||
irb (~> 1.13)
|
||||
parser
|
||||
rails (>= 7.0)
|
||||
rainbow
|
||||
countries (7.1.1)
|
||||
unaccent (~> 0.3)
|
||||
crass (1.0.6)
|
||||
date (3.4.1)
|
||||
debug (1.10.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
domain_name (0.6.20240107)
|
||||
doorkeeper (5.8.2)
|
||||
railties (>= 5)
|
||||
dotenv (3.1.8)
|
||||
drb (2.2.3)
|
||||
dry-cli (1.2.0)
|
||||
erb (5.0.1)
|
||||
erb_lint (0.9.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
parser (>= 2.7.1.4)
|
||||
rainbow
|
||||
rubocop (>= 1)
|
||||
smart_properties
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
faraday (2.13.1)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-mashify (1.0.0)
|
||||
faraday (~> 2.0)
|
||||
hashie
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (3.4.0)
|
||||
net-http (>= 0.5.0)
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-aarch64-linux-musl)
|
||||
ffi (1.17.2-arm-linux-gnu)
|
||||
ffi (1.17.2-arm-linux-musl)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
ffi (1.17.2-x86_64-darwin)
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
ffi (1.17.2-x86_64-linux-musl)
|
||||
ffi-compiler (1.3.2)
|
||||
ffi (>= 1.15.5)
|
||||
rake
|
||||
fiddle (1.1.8)
|
||||
flipper (1.3.5)
|
||||
concurrent-ruby (< 2)
|
||||
flipper-active_record (1.3.5)
|
||||
activerecord (>= 4.2, < 9)
|
||||
flipper (~> 1.3.5)
|
||||
flipper-ui (1.3.5)
|
||||
erubi (>= 1.0.0, < 2.0.0)
|
||||
flipper (~> 1.3.5)
|
||||
rack (>= 1.4, < 4)
|
||||
rack-protection (>= 1.5.3, < 5.0.0)
|
||||
rack-session (>= 1.0.2, < 3.0.0)
|
||||
sanitize (< 8)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
gli (2.22.2)
|
||||
ostruct
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
good_job (4.10.2)
|
||||
activejob (>= 6.1.0)
|
||||
activerecord (>= 6.1.0)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
fugit (>= 1.11.0)
|
||||
railties (>= 6.1.0)
|
||||
thor (>= 1.0.0)
|
||||
hashid-rails (1.4.1)
|
||||
activerecord (>= 4.0)
|
||||
hashids (~> 1.0)
|
||||
hashids (1.0.6)
|
||||
hashie (5.0.0)
|
||||
honeybadger (5.28.0)
|
||||
logger
|
||||
ostruct
|
||||
http (5.2.0)
|
||||
addressable (~> 2.8)
|
||||
base64 (~> 0.1)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.2)
|
||||
llhttp-ffi (~> 0.5.0)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.3.0)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.14.0)
|
||||
mini_magick (>= 4.9.5, < 6)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.8.0)
|
||||
irb (1.15.2)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jb (0.8.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.12.0)
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.2)
|
||||
kaminari-activerecord (= 1.2.2)
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-actionview (1.2.2)
|
||||
actionview
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-activerecord (1.2.2)
|
||||
activerecord
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.1.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
logger (~> 1.6)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
actionmailer (>= 6.1)
|
||||
letter_opener (~> 1.9)
|
||||
railties (>= 6.1)
|
||||
rexml
|
||||
lint_roller (1.1.0)
|
||||
literal (1.7.1)
|
||||
zeitwerk
|
||||
llhttp-ffi (0.5.1)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
lockbox (2.0.1)
|
||||
logger (1.7.0)
|
||||
loofah (2.24.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
lz_string (0.3.0)
|
||||
mail (2.8.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
mini-levenshtein (0.1.2)
|
||||
mini_magick (5.2.0)
|
||||
benchmark
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.5)
|
||||
msgpack (1.8.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.5.8)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
ostruct (0.6.1)
|
||||
paper_trail (16.0.0)
|
||||
activerecord (>= 6.1)
|
||||
request_store (~> 1.4)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.9)
|
||||
phlex (2.2.1)
|
||||
zeitwerk (~> 2.7)
|
||||
phlex-rails (2.2.0)
|
||||
phlex (~> 2.2.1)
|
||||
railties (>= 7.1, < 9)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.4.0)
|
||||
propshaft (1.1.0)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
railties (>= 7.0.0)
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_activity (3.0.1)
|
||||
actionpack (>= 6.1.0)
|
||||
activerecord (>= 6.1)
|
||||
i18n (>= 0.5.0)
|
||||
railties (>= 6.1.0)
|
||||
public_suffix (6.0.2)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.5.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.15)
|
||||
rack-protection (4.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.2)
|
||||
actioncable (= 8.0.2)
|
||||
actionmailbox (= 8.0.2)
|
||||
actionmailer (= 8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
actiontext (= 8.0.2)
|
||||
actionview (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activemodel (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activestorage (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.2)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails_semantic_logger (4.17.0)
|
||||
rack
|
||||
railties (>= 5.1)
|
||||
semantic_logger (~> 4.16)
|
||||
railties (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rdoc (6.14.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
redcarpet (3.6.1)
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.6.1)
|
||||
io-console (~> 0.5)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
rexml (3.4.1)
|
||||
rinku (2.0.6)
|
||||
rouge (4.5.2)
|
||||
rubocop (1.75.7)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.44.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-performance (1.25.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-rails (2.32.0)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rails-omakase (1.1.0)
|
||||
rubocop (>= 1.72)
|
||||
rubocop-performance (>= 1.24)
|
||||
rubocop-rails (>= 2.30)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.4)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
sanitize (7.0.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.16.8)
|
||||
securerandom (0.4.1)
|
||||
semantic_logger (4.16.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
serve_byte_range (1.0.0)
|
||||
rack (>= 1.0)
|
||||
slack-ruby-client (2.6.0)
|
||||
faraday (>= 2.0)
|
||||
faraday-mashify
|
||||
faraday-multipart
|
||||
gli
|
||||
hashie
|
||||
logger
|
||||
smart_properties (1.17.0)
|
||||
stringio (3.1.7)
|
||||
superform (0.5.1)
|
||||
phlex-rails (>= 1.0, < 3.0)
|
||||
zeitwerk (~> 2.6)
|
||||
thor (1.3.2)
|
||||
thruster (0.1.13)
|
||||
thruster (0.1.13-aarch64-linux)
|
||||
thruster (0.1.13-arm64-darwin)
|
||||
thruster (0.1.13-x86_64-darwin)
|
||||
thruster (0.1.13-x86_64-linux)
|
||||
timeout (0.4.3)
|
||||
turbo-rails (2.0.16)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unaccent (0.4.0)
|
||||
unicode-display_width (3.1.4)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uri (1.0.3)
|
||||
useragent (0.16.11)
|
||||
valid_email2 (7.0.13)
|
||||
activemodel (>= 6.0)
|
||||
mail (~> 2.5)
|
||||
vite_rails (3.0.19)
|
||||
railties (>= 5.1, < 9)
|
||||
vite_ruby (~> 3.0, >= 3.2.2)
|
||||
vite_ruby (3.9.2)
|
||||
dry-cli (>= 0.7, < 2)
|
||||
logger (~> 1.6)
|
||||
mutex_m
|
||||
rack-proxy (~> 0.6, >= 0.6.1)
|
||||
zeitwerk (~> 2.2)
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
websocket-driver (0.7.7)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wicked (2.0.0)
|
||||
railties (>= 3.0.7)
|
||||
zeitwerk (2.7.3)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
aarch64-linux-gnu
|
||||
aarch64-linux-musl
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
arm64-darwin
|
||||
x86_64-darwin
|
||||
x86_64-linux
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
aasm (~> 5.5)
|
||||
active_storage_encryption (~> 0.3.0)
|
||||
acts_as_paranoid (~> 0.10.3)
|
||||
annotaterb (~> 4.19)
|
||||
audits1984 (~> 0.1.7)
|
||||
awesome_print (~> 1.9)
|
||||
aws-sdk-s3 (~> 1.189)
|
||||
blind_index (~> 2.7)
|
||||
bootsnap
|
||||
brakeman
|
||||
console1984 (~> 0.2.2)
|
||||
countries (~> 7.1)
|
||||
debug
|
||||
doorkeeper (~> 5.8)
|
||||
dotenv
|
||||
erb_lint (~> 0.9.0)
|
||||
faraday (~> 2.13)
|
||||
flipper (~> 1.3)
|
||||
flipper-active_record (~> 1.3)
|
||||
flipper-ui (~> 1.3)
|
||||
good_job (~> 4.10)
|
||||
hashid-rails (~> 1.4)
|
||||
honeybadger (~> 5.28)
|
||||
http (~> 5.2)
|
||||
image_processing (~> 1.2)
|
||||
jb (~> 0.8.2)
|
||||
kaminari (~> 1.2)
|
||||
letter_opener_web (~> 3.0)
|
||||
literal (~> 1.7)
|
||||
lockbox (~> 2.0)
|
||||
lz_string (~> 0.3.0)
|
||||
mini-levenshtein (~> 0.1.2)
|
||||
paper_trail (~> 16.0)
|
||||
pg (~> 1.1)
|
||||
phlex (~> 2.2)
|
||||
phlex-rails (~> 2.2)
|
||||
propshaft (~> 1.1)
|
||||
public_activity (~> 3.0)
|
||||
puma (>= 5.0)
|
||||
pundit (~> 2.5)
|
||||
rails (~> 8.0.2)
|
||||
rails_semantic_logger (~> 4.17)
|
||||
redcarpet (~> 3.6)
|
||||
rubocop-rails-omakase
|
||||
ruby-vips (~> 2.2)
|
||||
slack-ruby-client (~> 2.6)
|
||||
superform (~> 0.5.1)
|
||||
thruster
|
||||
tzinfo-data
|
||||
valid_email2 (~> 7.0)
|
||||
vite_rails
|
||||
web-console
|
||||
wicked (~> 2.0)
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.9
|
||||
3
Procfile.dev
Normal file
3
Procfile.dev
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
vite: bin/vite dev
|
||||
web: bin/rails s
|
||||
29
README.md
Normal file
29
README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Identity Vault
|
||||
|
||||
This is the Rails codebase powering https://identity.hackclub.com!
|
||||
|
||||
## contributing:
|
||||
poke [nora](https://hackclub.slack.com/team/U06QK6AG3RD)!
|
||||
avoid questions that can be answered by reading the source code, but otherwise i'd be happy to help you get up to speed :-D
|
||||
|
||||
kindly `bin/lint` your code before you submit it!
|
||||
### areas of focus:
|
||||
the ops view components (look in `app/components`) are a hot mess...
|
||||
|
||||
so is the onboarding controller, she should really be ripped out and replaced.
|
||||
|
||||
## dev setup:
|
||||
- make sure you have working installations of ruby ≥ 3.4.4 & nodejs
|
||||
- clone repo
|
||||
- create .env.development, populate `DATABASE_URL` w/ a local postgres instance
|
||||
- run `bundle install`
|
||||
- run `rails db:prepare`
|
||||
- console in (`bin/rails console`)
|
||||
- `Backend::User.create!(slack_id: "U<whatever>", username: "<you>", active: true, super_admin: true)`
|
||||
- run `bin/dev` (and `bin/vite dev` if you want hot reload on css & js)
|
||||
- visit `http://localhost:3000/backend/login`, paste that Slack ID in, and "fake it til' you make it"
|
||||
|
||||
## security
|
||||
|
||||
this oughta go without saying, but if you find a security-relevant issue please either contact me directly or go through the security.hackclub.com flow –
|
||||
if you just open an issue or a PR there's a chance a bad actor sees it and exploits it before we can patch or merge.
|
||||
6
Rakefile
Normal file
6
Rakefile
Normal file
|
|
@ -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
|
||||
1
app/assets/stylesheets/application.css
Normal file
1
app/assets/stylesheets/application.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* Application styles */
|
||||
24
app/components/api_example.rb
Normal file
24
app/components/api_example.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
class Components::APIExample < Components::Base
|
||||
extend Literal::Properties
|
||||
prop :method, _Nilable(String)
|
||||
prop :url, _Nilable(String)
|
||||
prop :path_only, _Boolean?
|
||||
|
||||
def view_template
|
||||
div style: { margin: "10px 0" } do
|
||||
code style: { background: "black", padding: "0.2em", color: "white" } do
|
||||
span style: { color: "cyan" } do
|
||||
@method
|
||||
end
|
||||
plain " "
|
||||
copy_to_clipboard @url do
|
||||
if @path_only
|
||||
CGI.unescape(URI.parse(@url).tap { |u| u.host = u.scheme = u.port = nil }.to_s)
|
||||
else
|
||||
@url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
24
app/components/base.rb
Normal file
24
app/components/base.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::Base < Phlex::HTML
|
||||
include Components
|
||||
|
||||
# Include any helpers you want to be available across all components
|
||||
include Phlex::Rails::Helpers::Routes
|
||||
include Phlex::Rails::Helpers::FormWith
|
||||
include Phlex::Rails::Helpers::DistanceOfTimeInWords
|
||||
|
||||
# Register Rails form helpers
|
||||
register_value_helper :form_authenticity_token
|
||||
register_value_helper :dev_tool
|
||||
register_output_helper :vite_image_tag
|
||||
register_value_helper :ap
|
||||
register_output_helper :copy_to_clipboard
|
||||
|
||||
if Rails.env.development?
|
||||
def before_template
|
||||
comment { "Before #{self.class.name}" }
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/components/bootleg_turbo.rb
Normal file
17
app/components/bootleg_turbo.rb
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class Components::BootlegTurbo < Components::Base
|
||||
def initialize(path, text: nil, **opts)
|
||||
@path = path
|
||||
@text = text
|
||||
@opts = opts
|
||||
end
|
||||
|
||||
def view_template
|
||||
div(hx_get: @path, hx_trigger: :load, **@opts) do
|
||||
if @text
|
||||
plain @text
|
||||
br
|
||||
end
|
||||
vite_image_tag "images/loader.gif", class: :htmx_indicator, style: "image-rendering: pixelated;"
|
||||
end
|
||||
end
|
||||
end
|
||||
37
app/components/brand.rb
Normal file
37
app/components/brand.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
class Components::Brand < Components::Base
|
||||
def initialize(identity:)
|
||||
@identity = identity
|
||||
end
|
||||
|
||||
def view_template
|
||||
div(class: "brand") do
|
||||
if @identity.present?
|
||||
copy_to_clipboard @identity.public_id, tooltip_direction: "e", label: "click to copy your internal ID" do
|
||||
logo
|
||||
end
|
||||
else
|
||||
logo
|
||||
end
|
||||
h1 { "Hack Club Identity" }
|
||||
end
|
||||
button id: "lightswitch", class: "lightswitch-btn", type: "button", "aria-label": "Toggle theme" do
|
||||
span class: "lightswitch-icon" do
|
||||
"🌙"
|
||||
end
|
||||
end
|
||||
case Rails.env
|
||||
when "staging"
|
||||
div(class: "banner purple") do
|
||||
safe "this is a staging environment. <b>do not upload any actual personal information here.<b>"
|
||||
end
|
||||
when "development"
|
||||
div(class: "banner success") do
|
||||
plain "you're in dev! go nuts :3"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def logo
|
||||
vite_image_tag "images/hc-square.png", alt: "Hack Club logo", class: "brand-logo"
|
||||
end
|
||||
end
|
||||
51
app/components/break_the_glass.rb
Normal file
51
app/components/break_the_glass.rb
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::BreakTheGlass < Components::Base
|
||||
attr_reader :break_glassable, :auto_break_glass
|
||||
|
||||
def initialize(break_glassable, auto_break_glass: nil)
|
||||
@break_glassable = break_glassable
|
||||
@auto_break_glass = auto_break_glass
|
||||
end
|
||||
|
||||
def view_template
|
||||
if glass_broken?
|
||||
yield if block_given?
|
||||
else
|
||||
render_break_the_glass
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def glass_broken?
|
||||
return false unless helpers.user_signed_in?
|
||||
|
||||
# Check if a recent break glass record already exists
|
||||
existing_record = BreakGlassRecord.for_user_and_document(helpers.current_user, @break_glassable)
|
||||
.recent
|
||||
.exists?
|
||||
|
||||
return true if existing_record
|
||||
|
||||
# If auto_break_glass is enabled, automatically create a break glass record
|
||||
if @auto_break_glass
|
||||
BreakGlassRecord.create!(
|
||||
backend_user: helpers.current_user,
|
||||
break_glassable: @break_glassable,
|
||||
reason: @auto_break_glass,
|
||||
accessed_at: Time.current,
|
||||
automatic: true,
|
||||
)
|
||||
return true
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def render_break_the_glass
|
||||
div do
|
||||
render Components::BreakTheGlassForm.new(@break_glassable)
|
||||
end
|
||||
end
|
||||
end
|
||||
46
app/components/break_the_glass_form.rb
Normal file
46
app/components/break_the_glass_form.rb
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::BreakTheGlassForm < Components::Base
|
||||
def initialize(break_glassable)
|
||||
@break_glassable = break_glassable
|
||||
end
|
||||
|
||||
def view_template
|
||||
div(class: "break-glass-form", style: "padding: 2rem;") do
|
||||
div(style: "margin-bottom: 1rem;") do
|
||||
vite_image_tag "images/icons/break-the-glass.png", style: "width: 64px; image-rendering: pixelated;"
|
||||
div(style: "display: inline-block; vertical-align: top; margin-left: 0.5em;") do
|
||||
h1(style: "margin 0; display: inline-block; vertical-align: top;") { "Break the Glass" }
|
||||
br
|
||||
plain "This #{document_type} has already been reviewed."
|
||||
br
|
||||
plain "Please affirm that you have a legitimate need to view this #{document_type}."
|
||||
end
|
||||
end
|
||||
|
||||
form_with url: "/backend/break_glass", method: :post, local: true, style: "max-width: 400px; margin: 0 auto;" do |form|
|
||||
form.hidden_field :break_glassable_id, value: @break_glassable.id
|
||||
form.hidden_field :break_glassable_type, value: @break_glassable.class.name
|
||||
|
||||
div(style: "display: flex; align-items: center; gap: 0.5em;") do
|
||||
p { "I'm accessing this #{document_type} " }
|
||||
form.text_field :reason, placeholder: "because i'm investigating a fraud claim", style: "width: 30%;"
|
||||
form.submit "i promise.", class: "button button-primary"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def document_type
|
||||
case @break_glassable.class.name
|
||||
when "Identity::Document"
|
||||
"identity document"
|
||||
when "Identity::AadhaarRecord"
|
||||
"aadhaar record"
|
||||
else
|
||||
"document"
|
||||
end
|
||||
end
|
||||
end
|
||||
43
app/components/footer.rb
Normal file
43
app/components/footer.rb
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
class Components::Footer < Components::Base
|
||||
include Phlex::Rails::Helpers::TimeAgoInWords
|
||||
|
||||
def view_template
|
||||
footer(class: "app-footer") do
|
||||
div(class: "footer-content") do
|
||||
div(class: "footer-main") do
|
||||
p(class: "app-name") { "Identity Vault" }
|
||||
end
|
||||
|
||||
div(class: "footer-version") do
|
||||
div(class: "version-info") do
|
||||
p do
|
||||
plain "Build "
|
||||
if git_version.present?
|
||||
if commit_link.present?
|
||||
a(href: commit_link, target: "_blank", class: "version-link") do
|
||||
"v#{git_version}"
|
||||
end
|
||||
else
|
||||
span(class: "version-text") { "v#{git_version}" }
|
||||
end
|
||||
end
|
||||
plain " from #{time_ago_in_words(server_start_time)} ago"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
div(class: "environment-badge #{Rails.env.downcase}") do
|
||||
Rails.env.upcase
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def git_version = Rails.application.config.try(:git_version)
|
||||
|
||||
def commit_link = Rails.application.config.try(:commit_link)
|
||||
|
||||
def server_start_time
|
||||
Rails.application.config.try(:server_start_time)
|
||||
end
|
||||
end
|
||||
9
app/components/home_button.rb
Normal file
9
app/components/home_button.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::HomeButton < Components::Base
|
||||
def view_template
|
||||
a href: root_path do
|
||||
"← back to home"
|
||||
end
|
||||
end
|
||||
end
|
||||
50
app/components/identity.rb
Normal file
50
app/components/identity.rb
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
class Components::Identity < Components::Base
|
||||
attr_reader :identity
|
||||
|
||||
def initialize(identity, show_legal_name: false)
|
||||
@identity = identity
|
||||
@show_legal_name = show_legal_name
|
||||
end
|
||||
|
||||
def field(label, value = nil)
|
||||
b { "#{label}: " }
|
||||
if block_given?
|
||||
yield
|
||||
else
|
||||
span { value.to_s }
|
||||
end
|
||||
br
|
||||
end
|
||||
|
||||
def view_template
|
||||
div do
|
||||
render @identity
|
||||
br
|
||||
if @identity.legal_first_name.present? && @show_legal_name
|
||||
field "Legal First Name", @identity.legal_first_name
|
||||
field "Legal Last Name", @identity.legal_last_name
|
||||
end
|
||||
field "Country", @identity.country
|
||||
field "Primary Email", @identity.primary_email
|
||||
field "Birthday", @identity.birthday
|
||||
field "Phone", @identity.phone_number
|
||||
field "Verification status", @identity.verification_status.humanize
|
||||
if defined?(@identity.ysws_eligible) && !@identity.ysws_eligible.nil?
|
||||
field "YSWS eligible", @identity.ysws_eligible
|
||||
end
|
||||
|
||||
field "Slack ID" do
|
||||
if identity.slack_id.present?
|
||||
a(href: "https://hackclub.slack.com/team/#{identity.slack_id}") do
|
||||
identity.slack_id
|
||||
end
|
||||
copy_to_clipboard(identity.slack_id) do
|
||||
plain " (copy)"
|
||||
end
|
||||
else
|
||||
plain "not set"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
app/components/identity_review/aadhaar_full.rb
Normal file
43
app/components/identity_review/aadhaar_full.rb
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
class Components::IdentityReview::AadhaarFull < Components::Base
|
||||
def initialize(aadhaar_record)
|
||||
@aadhaar_record = aadhaar_record
|
||||
@data = @aadhaar_record.doc_json[:data]
|
||||
end
|
||||
|
||||
def field(key, name)
|
||||
res = @data.dig(key)
|
||||
return if res.blank?
|
||||
li do
|
||||
b { "#{name}: " }
|
||||
plain res
|
||||
end
|
||||
end
|
||||
|
||||
def view_template
|
||||
h2 { "Full Aadhaar data:" }
|
||||
br
|
||||
|
||||
ul style: { list_style_type: "disc" } do
|
||||
li do
|
||||
b { "Photo:" }
|
||||
br
|
||||
img src: "data:image/jpeg;base64,#{@data[:photo]}", style: { width: "100px", margin_left: "1em" }
|
||||
end
|
||||
field :name, "Full name"
|
||||
field :"Father Name", "Father's name"
|
||||
field :dob, "Date of birth"
|
||||
field :aadhar_number, "Aadhaar number"
|
||||
field :gender, "Assigned gender"
|
||||
field :co, "C/O"
|
||||
li do
|
||||
b { "Address:" }
|
||||
ul style: { margin_left: "1rem", list_style_type: "square" } do
|
||||
@data.dig(:address).each do |key, value|
|
||||
next if value.blank?
|
||||
li { b { "#{key}: " }; plain value }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
21
app/components/identity_review/aadhaar_info.rb
Normal file
21
app/components/identity_review/aadhaar_info.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
class Components::IdentityReview::AadhaarInfo < Components::Base
|
||||
def initialize(verification)
|
||||
@verification = verification
|
||||
end
|
||||
|
||||
def view_template
|
||||
div class: "lowered padding" do
|
||||
h2(style: "margin-top: 0;") { "Aadhaar Information" }
|
||||
table style: "width: 100%;" do
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Aadhaar Number:" }
|
||||
td(style: "padding: 0.25rem 0;") { @verification.identity.aadhaar_number }
|
||||
end
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Uploaded:" }
|
||||
td(style: "padding: 0.25rem 0;") { @verification.pending_at&.strftime("%B %d, %Y at %I:%M %p") || "N/A" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
41
app/components/identity_review/basic_details.rb
Normal file
41
app/components/identity_review/basic_details.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
class Components::IdentityReview::BasicDetails < Components::Base
|
||||
def initialize(identity)
|
||||
@identity = identity
|
||||
end
|
||||
|
||||
def view_template
|
||||
div class: "lowered padding" do
|
||||
h2(style: "margin-top: 0;") { "Identity Information" }
|
||||
table style: "width: 100%;" do
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Name:" }
|
||||
td(style: "padding: 0.25rem 0;") { render(@identity) }
|
||||
end
|
||||
if @identity.legal_first_name.present?
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Legal Name:" }
|
||||
td(style: "padding: 0.25rem 0;") {
|
||||
"#{@identity.legal_first_name} #{@identity.legal_last_name}"
|
||||
}
|
||||
end
|
||||
end
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Email:" }
|
||||
td(style: "padding: 0.25rem 0;") { @identity.primary_email }
|
||||
end
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Birthday:" }
|
||||
td(style: "padding: 0.25rem 0;") { @identity.birthday.strftime("%B %d, %Y") }
|
||||
end
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Age:" }
|
||||
td(style: "padding: 0.25rem 0;") { @identity.age.round(2) }
|
||||
end
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Country:" }
|
||||
td(style: "padding: 0.25rem 0;") { @identity.country }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
118
app/components/identity_review/document_files.rb
Normal file
118
app/components/identity_review/document_files.rb
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
class Components::IdentityReview::DocumentFiles < Components::Base
|
||||
def initialize(document)
|
||||
@document = document
|
||||
end
|
||||
|
||||
def view_template
|
||||
h2(style: "margin-top: 0;") { "Document Files" }
|
||||
|
||||
if @document.files.attached?
|
||||
@document.files.each_with_index do |file, index|
|
||||
div(style: "margin-bottom: 2rem;") do
|
||||
h3 { "File #{index + 1}: #{file.filename}" }
|
||||
|
||||
if file.content_type.start_with?("image/")
|
||||
# Display image files (use variants for format conversion)
|
||||
image_src = if file.content_type.in?(%w[image/heic image/heif])
|
||||
helpers.url_for(file.variant(format: :png))
|
||||
else
|
||||
helpers.url_for(file)
|
||||
end
|
||||
|
||||
div(style: "position: relative; display: inline-block;") do
|
||||
# Loader
|
||||
div(
|
||||
id: "loader-#{index}",
|
||||
style: "display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;",
|
||||
) do
|
||||
div(style: "text-align: center;") do
|
||||
plain file.content_type.in?(%w[image/heic image/heif]) ? "Converting HEIC image..." : "Loading image..."
|
||||
br
|
||||
vite_image_tag "images/loader.gif", style: "image-rendering: pixelated; width: 32px; height: 32px;"
|
||||
end
|
||||
end
|
||||
|
||||
# Image container (hidden until loaded)
|
||||
div(id: "image-container-#{index}", style: "display: none;") do
|
||||
img(
|
||||
src: image_src,
|
||||
alt: "Document file #{index + 1}",
|
||||
style: "max-width: 100%; max-height: 600px; height: auto; border: 1px solid #ddd; border-radius: 4px; transition: transform 0.3s ease;",
|
||||
id: "image-#{index}",
|
||||
data: { rotation: 0 },
|
||||
onload: safe("document.getElementById('loader-#{index}').style.display='none'; document.getElementById('image-container-#{index}').style.display='block';"),
|
||||
onerror: safe("document.getElementById('loader-#{index}').innerHTML='<p>Error loading image</p>';"),
|
||||
)
|
||||
button(
|
||||
type: "button",
|
||||
class: "button",
|
||||
style: "position: absolute; top: 10px; right: 10px;",
|
||||
onclick: safe("rotateImage(#{index})"),
|
||||
title: "Rotate image",
|
||||
) { "↻ Rotate" }
|
||||
end
|
||||
end
|
||||
elsif file.content_type == "application/pdf"
|
||||
# Display PDF files inline
|
||||
div(style: "border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;") do
|
||||
div(style: "padding: 1rem; border-bottom: 1px solid #ddd; background: #f5f5f5;") do
|
||||
span(style: "font-weight: bold;") { "📄 #{file.filename}" }
|
||||
a(
|
||||
href: helpers.url_for(file),
|
||||
target: "_blank",
|
||||
style: "color: #0066cc; text-decoration: underline; margin-left: 1rem;",
|
||||
) { "Open in new tab" }
|
||||
end
|
||||
# Embed PDF using iframe
|
||||
iframe(
|
||||
src: helpers.url_for(file),
|
||||
width: "100%",
|
||||
height: "600",
|
||||
style: "border: none; display: block;",
|
||||
type: "application/pdf",
|
||||
) do
|
||||
# Fallback for browsers that don't support PDF embedding
|
||||
p(style: "padding: 2rem; text-align: center;") do
|
||||
plain "Your browser doesn't support PDF embedding. "
|
||||
a(
|
||||
href: helpers.url_for(file),
|
||||
target: "_blank",
|
||||
style: "color: #0066cc; text-decoration: underline;",
|
||||
) { "Click here to view the PDF" }
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
# Display other file types
|
||||
div(style: "border: 1px solid #ddd; border-radius: 4px; padding: 1rem; background: #f9f9f9;") do
|
||||
p { "📁 File: #{file.filename}" }
|
||||
p { "Type: #{file.content_type}" }
|
||||
p do
|
||||
a(
|
||||
href: helpers.url_for(file),
|
||||
target: "_blank",
|
||||
style: "color: #0066cc; text-decoration: underline;",
|
||||
) { "Download File" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
p(style: "color: #666; font-style: italic;") { "No files attached to this document." }
|
||||
end
|
||||
|
||||
# Add JavaScript for image rotation
|
||||
script do
|
||||
raw safe <<~JAVASCRIPT
|
||||
function rotateImage(index) {
|
||||
const img = document.getElementById('image-' + index);
|
||||
let currentRotation = parseInt(img.dataset.rotation || 0);
|
||||
currentRotation = (currentRotation + 90);
|
||||
img.dataset.rotation = currentRotation;
|
||||
img.style.transform = 'rotate(' + currentRotation + 'deg)';
|
||||
}
|
||||
JAVASCRIPT
|
||||
end
|
||||
end
|
||||
end
|
||||
39
app/components/identity_review/document_info.rb
Normal file
39
app/components/identity_review/document_info.rb
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
class Components::IdentityReview::DocumentInfo < Components::Base
|
||||
def initialize(verification)
|
||||
@verification = verification
|
||||
end
|
||||
|
||||
def view_template
|
||||
div class: "lowered padding" do
|
||||
h2(style: "margin-top: 0;") { "Document Information" }
|
||||
table style: "width: 100%;" do
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Type:" }
|
||||
td(style: "padding: 0.25rem 0;") { @verification.document_type }
|
||||
end
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Uploaded:" }
|
||||
td(style: "padding: 0.25rem 0;") { @verification.identity_document.created_at.strftime("%B %d, %Y at %I:%M %p") }
|
||||
end
|
||||
if @verification.identity.country == "IN"
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Suggested Aadhaar password:" }
|
||||
td(style: "padding: 0.25rem 0;") { copy_to_clipboard(@verification.identity.suggested_aadhaar_password, tooltip_direction: "e") }
|
||||
end
|
||||
end
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Status:" }
|
||||
td(style: "padding: 0.25rem 0;") do
|
||||
span class: (@verification.pending? ? "status-pending" : @verification.approved? ? "status-verified" : "status-rejected") do
|
||||
@verification.status.humanize
|
||||
end
|
||||
end
|
||||
end
|
||||
tr do
|
||||
td(style: "font-weight: bold; padding: 0.25rem 0;") { "Files:" }
|
||||
td(style: "padding: 0.25rem 0;") { @verification.identity_document.files.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
22
app/components/inspector.rb
Normal file
22
app/components/inspector.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
class Components::Inspector < Components::Base
|
||||
def initialize(record, small: false)
|
||||
@record = record
|
||||
@small = small
|
||||
@id_line = "#{@record.class.name}#{" record" unless @small} #{@record&.try(:public_id) || @record&.id}"
|
||||
end
|
||||
|
||||
def view_template
|
||||
return unless Rails.env.development?
|
||||
|
||||
details(class: @small ? nil : "dev-tool") do
|
||||
summary { "#{"Inspect" unless @small} #{@id_line}" }
|
||||
pre class: %i[input readonly] do
|
||||
unless @record.nil?
|
||||
raw safe(ap @record)
|
||||
else
|
||||
"no record?"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/components/public_activity/container.rb
Normal file
23
app/components/public_activity/container.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
class Components::PublicActivity::Container < Components::Base
|
||||
register_value_helper :render_activities
|
||||
|
||||
def initialize(activities)
|
||||
@activities = activities
|
||||
end
|
||||
|
||||
def view_template
|
||||
table class: %i[table detailed] do
|
||||
thead do
|
||||
tr do
|
||||
th { "User" }
|
||||
th { "Action" }
|
||||
th { "Time" }
|
||||
th { "Inspect" } if Rails.env.development?
|
||||
end
|
||||
end
|
||||
tbody do
|
||||
render_activities(@activities)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
22
app/components/public_activity/snippet.rb
Normal file
22
app/components/public_activity/snippet.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::PublicActivity::Snippet < Components::Base
|
||||
def initialize(activity, owner: nil)
|
||||
@activity = activity
|
||||
@owner = owner
|
||||
end
|
||||
|
||||
def view_template
|
||||
tr do
|
||||
td { render @owner || @activity.owner }
|
||||
td { yield }
|
||||
td { @activity.created_at.strftime("%Y-%m-%d %H:%M:%S") }
|
||||
td do
|
||||
if Rails.env.development?
|
||||
render Components::Inspector.new(@activity, small: true)
|
||||
render Components::Inspector.new(@activity.trackable, small: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
app/components/resemblance.rb
Normal file
15
app/components/resemblance.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class Components::Resemblance < Components::Base
|
||||
attr_reader :resemblance
|
||||
|
||||
def initialize(resemblance)
|
||||
@resemblance = resemblance
|
||||
end
|
||||
|
||||
def view_template
|
||||
div style: { border: "1px solid", padding: "10px", margin: "10px" } do
|
||||
render @resemblance
|
||||
render Components::Identity.new(@resemblance.past_identity)
|
||||
render Components::Inspector.new(@resemblance)
|
||||
end
|
||||
end
|
||||
end
|
||||
27
app/components/user_mention.rb
Normal file
27
app/components/user_mention.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::UserMention < Components::Base
|
||||
extend Literal::Properties
|
||||
|
||||
prop :user, _Union(Backend::User, ::Identity), :positional
|
||||
|
||||
def view_template
|
||||
div class: "icon", role: "option" do
|
||||
case @user
|
||||
when Backend::User
|
||||
img src: @user.icon_url, width: "16px", class: "inline pr-2"
|
||||
div class: "icon-label" do
|
||||
a(href: backend_user_path(@user)) do
|
||||
span { @user.username }
|
||||
span { " ⚡" } if @user.super_admin?
|
||||
end
|
||||
end
|
||||
when ::Identity
|
||||
div(class: "inline pr-2") { "🪪" }
|
||||
div class: "icon-label" do
|
||||
a(href: backend_identity_path(@user)) { span { "#{@user.first_name} #{@user.last_name}" } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
27
app/components/window.rb
Normal file
27
app/components/window.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::Window < Components::Base
|
||||
extend Literal::Properties
|
||||
|
||||
prop :window_title, String, :positional
|
||||
prop :close_url, _Nilable(String)
|
||||
prop :maximize_url, _Nilable(String)
|
||||
prop :max_width, Integer, default: 400.freeze
|
||||
|
||||
def view_template
|
||||
div class: "window active", style: "max-width: #{@max_width}px" do
|
||||
div class: "title-bar" do
|
||||
div(class: "title-bar-text") { @window_title }
|
||||
if @close_url || @maximize_url
|
||||
div class: "title-bar-buttons" do
|
||||
button(data_maximize: "", onclick: safe("window.location.href='#{@maximize_url}'")) if @maximize_url
|
||||
button(data_close: "", onclick: safe("window.location.href='#{@close_url}'")) if @close_url
|
||||
end
|
||||
end
|
||||
end
|
||||
div class: "window-body" do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
41
app/controllers/aadhaar_controller.rb
Normal file
41
app/controllers/aadhaar_controller.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AadhaarController < ApplicationController
|
||||
before_action :ensure_step, :set_verification
|
||||
layout false
|
||||
|
||||
def async_digilocker_link
|
||||
begin
|
||||
@verification.generate_link!(
|
||||
callback_url: webhooks_aadhaar_callback_url(
|
||||
Rails.application.credentials.dig(:aadhaar, :webhook_secret)
|
||||
),
|
||||
redirect_url: submitted_onboarding_url,
|
||||
) unless @verification.aadhaar_external_transaction_id.present?
|
||||
|
||||
render :digilocker_link
|
||||
rescue StandardError => e
|
||||
uuid = Honeybadger.notify(e)
|
||||
response.set_header("HX-Retarget", "#async_flash")
|
||||
render "shared/async_flash", locals: { f: { error: "error generating digilocker link – #{e.message} #{uuid}" } }
|
||||
end
|
||||
end
|
||||
|
||||
def digilocker_redirect
|
||||
redirect_to @verification.aadhaar_link, allow_other_host: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_verification
|
||||
@verification = current_identity.aadhaar_verifications.draft.first
|
||||
end
|
||||
|
||||
def ensure_step
|
||||
render html: "🥐" unless current_identity&.onboarding_step == :aadhaar
|
||||
|
||||
if current_identity&.verification_status == "ineligible"
|
||||
redirect_to submitted_onboarding_path and return
|
||||
end
|
||||
end
|
||||
end
|
||||
81
app/controllers/addresses_controller.rb
Normal file
81
app/controllers/addresses_controller.rb
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
class AddressesController < ApplicationController
|
||||
include IsSneaky
|
||||
before_action :set_address, only: [ :show, :edit, :update, :destroy ]
|
||||
before_action :hide_some_data_away, only: %i[program_create_address]
|
||||
def index
|
||||
@addresses = current_identity.addresses
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
build_address
|
||||
end
|
||||
|
||||
def create
|
||||
@address = current_identity.addresses.new(address_params)
|
||||
if @address.save
|
||||
if current_identity.primary_address.nil?
|
||||
current_identity.update(primary_address: @address)
|
||||
end
|
||||
if params[:address][:from_program] == "true"
|
||||
redirect_to safe_redirect_url("address_return_to") || addresses_path, notice: "address created successfully!", allow_other_host: true
|
||||
else
|
||||
redirect_to addresses_path, notice: "address created successfully!"
|
||||
end
|
||||
else
|
||||
render params[:address][:from_program] == "true" ? :program_create_address : :new
|
||||
end
|
||||
end
|
||||
|
||||
def program_create_address
|
||||
build_address
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if params[:make_primary] == "true"
|
||||
current_identity.update(primary_address: @address)
|
||||
redirect_to addresses_path, notice: "Primary address updated!"
|
||||
elsif @address.update(address_params)
|
||||
redirect_to addresses_path, notice: "address updated successfully!"
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if current_identity.primary_address == @address
|
||||
if Rails.env.production?
|
||||
flash[:alert] = "can't delete your primary address..."
|
||||
redirect_to addresses_path
|
||||
return
|
||||
else
|
||||
current_identity.update(primary_address: nil)
|
||||
end
|
||||
end
|
||||
@address.destroy
|
||||
redirect_to addresses_path, notice: "address deleted successfully!"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_address
|
||||
@address = current_identity.addresses.build(
|
||||
country: current_identity.country,
|
||||
first_name: current_identity.first_name,
|
||||
last_name: current_identity.last_name,
|
||||
)
|
||||
end
|
||||
|
||||
def set_address
|
||||
@address = current_identity.addresses.find(params[:id])
|
||||
end
|
||||
|
||||
def address_params
|
||||
params.require(:address).permit(:first_name, :last_name, :line_1, :line_2, :city, :state, :postal_code, :country)
|
||||
end
|
||||
end
|
||||
6
app/controllers/api/external/application_controller.rb
vendored
Normal file
6
app/controllers/api/external/application_controller.rb
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module API
|
||||
module External
|
||||
class ApplicationController < ActionController::API
|
||||
end
|
||||
end
|
||||
end
|
||||
35
app/controllers/api/external/identities_controller.rb
vendored
Normal file
35
app/controllers/api/external/identities_controller.rb
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
module API
|
||||
module External
|
||||
class IdentitiesController < ApplicationController
|
||||
def check
|
||||
ident = if (public_id = params[:idv_id]).present?
|
||||
Identity.find_by_public_id(public_id)
|
||||
elsif (primary_email = params[:email]).present?
|
||||
Identity.find_by(primary_email:)
|
||||
elsif (slack_id = params[:slack_id]).present?
|
||||
Identity.find_by(slack_id:)
|
||||
else
|
||||
raise ActionController::ParameterMissing, "provide one of: idv_id, email, slack_id"
|
||||
end
|
||||
|
||||
result = if ident
|
||||
case ident.verification_status
|
||||
when "needs_submission", "pending"
|
||||
ident.verification_status
|
||||
when "verified"
|
||||
ident.ysws_eligible? ? "verified_eligible" : "verified_but_over_18"
|
||||
when "ineligible"
|
||||
"rejected"
|
||||
else
|
||||
"unknown"
|
||||
end
|
||||
else
|
||||
"not_found"
|
||||
end
|
||||
render json: {
|
||||
result:
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
48
app/controllers/api/v1/application_controller.rb
Normal file
48
app/controllers/api/v1/application_controller.rb
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
module API
|
||||
module V1
|
||||
class ApplicationController < ActionController::API
|
||||
prepend_view_path "app/views/api/v1"
|
||||
|
||||
helper_method :current_identity, :current_program, :current_scopes, :acting_as_program
|
||||
|
||||
attr_reader :current_identity
|
||||
attr_reader :current_program
|
||||
attr_reader :current_scopes
|
||||
attr_reader :acting_as_program
|
||||
|
||||
before_action :authenticate!
|
||||
|
||||
include ActionController::HttpAuthentication::Token::ControllerMethods
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError do |e|
|
||||
render json: { error: "not_authorized" }, status: :forbidden
|
||||
end
|
||||
|
||||
rescue_from ActionController::ParameterMissing do |e|
|
||||
render json: { error: e.message }, status: :bad_request
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authenticate!
|
||||
@current_token = authenticate_with_http_token do |t, _options|
|
||||
OAuthToken.find_by(token: t) || Program.find_by(program_key: t)
|
||||
end
|
||||
unless @current_token&.active?
|
||||
return render json: { error: "invalid_auth" }, status: :unauthorized
|
||||
end
|
||||
if @current_token.is_a?(OAuthToken)
|
||||
@current_identity = @current_token.resource_owner
|
||||
@current_program = @current_token.application
|
||||
unless @current_program&.active?
|
||||
return render json: { error: "invalid_auth" }, status: :unauthorized
|
||||
end
|
||||
else
|
||||
@acting_as_program = true
|
||||
@current_program = @current_token
|
||||
end
|
||||
@current_scopes = @current_program.scopes
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
11
app/controllers/api/v1/hcb_controller.rb
Normal file
11
app/controllers/api/v1/hcb_controller.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
module API
|
||||
module V1
|
||||
class HCBController < ApplicationController
|
||||
skip_before_action :authenticate!
|
||||
|
||||
def show
|
||||
render json: { pending: Verification.where(status: "pending").count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
11
app/controllers/api/v1/health_check_controller.rb
Normal file
11
app/controllers/api/v1/health_check_controller.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
module API
|
||||
module V1
|
||||
class HealthCheckController < ApplicationController
|
||||
skip_before_action :authenticate!
|
||||
def show
|
||||
_ = Identity.last
|
||||
render json: { message: "we're chillin'" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
42
app/controllers/api/v1/identities_controller.rb
Normal file
42
app/controllers/api/v1/identities_controller.rb
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
module API
|
||||
module V1
|
||||
class IdentitiesController < ApplicationController
|
||||
def me
|
||||
@identity = current_identity
|
||||
raise ActiveRecord::RecordNotFound unless current_identity
|
||||
render :me
|
||||
end
|
||||
|
||||
def show
|
||||
raise Pundit::NotAuthorizedError unless acting_as_program
|
||||
@identity = ident_scope.find_by_public_id!(params[:id])
|
||||
render :show
|
||||
end
|
||||
|
||||
def set_slack_id
|
||||
raise Pundit::NotAuthorizedError unless acting_as_program && current_scopes.include?("set_slack_id")
|
||||
@identity = ident_scope.find_by_public_id!(params[:id])
|
||||
|
||||
if @identity.slack_id.present?
|
||||
return render json: { message: "slack already associated?" }
|
||||
end
|
||||
|
||||
@identity.update!(slack_id: params.require(:slack_id))
|
||||
@identity.create_activity(key: "identity.set_slack_id", owner: current_program)
|
||||
render :show
|
||||
end
|
||||
|
||||
def index
|
||||
raise Pundit::NotAuthorizedError unless acting_as_program
|
||||
@identities = ident_scope.all
|
||||
render :index
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ident_scope
|
||||
current_program.identities
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
66
app/controllers/application_controller.rb
Normal file
66
app/controllers/application_controller.rb
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
class ApplicationController < ActionController::Base
|
||||
include PublicActivity::StoreController
|
||||
include IsSneaky
|
||||
|
||||
helper_method :current_identity, :identity_signed_in?, :current_onboarding_step
|
||||
|
||||
before_action :invalidate_v1_sessions, :authenticate_identity!, :set_honeybadger_context
|
||||
|
||||
before_action :set_paper_trail_whodunnit
|
||||
|
||||
def current_identity
|
||||
@current_identity ||= Identity.find_by(id: session[:identity_id]) if session[:identity_id]
|
||||
end
|
||||
|
||||
alias_method :user_for_public_activity, :current_identity
|
||||
|
||||
def user_for_paper_trail = current_identity&.id
|
||||
|
||||
def identity_signed_in? = !!current_identity
|
||||
|
||||
|
||||
def invalidate_v1_sessions
|
||||
if cookies["_identity_vault_session"]
|
||||
cookies.delete("_identity_vault_session",
|
||||
path: "/",
|
||||
secure: Rails.env.production?,
|
||||
httponly: true)
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_identity!
|
||||
unless identity_signed_in?
|
||||
session[:oauth_return_to] = request.original_url unless request.xhr?
|
||||
# JANK ALERT
|
||||
hide_some_data_away
|
||||
|
||||
# EW
|
||||
return if controller_name == "onboardings"
|
||||
|
||||
redirect_to welcome_onboarding_path
|
||||
end
|
||||
end
|
||||
|
||||
def set_honeybadger_context
|
||||
Honeybadger.context({
|
||||
identity_id: current_identity&.id
|
||||
})
|
||||
end
|
||||
|
||||
def current_onboarding_step
|
||||
identity = current_identity
|
||||
|
||||
return :basic_info unless identity&.persisted?
|
||||
return :document unless identity.verifications.where(status: [ "approved", "pending" ]).any?
|
||||
|
||||
:submitted
|
||||
rescue => e
|
||||
Rails.logger.error "Error determining onboarding step: #{e.message}"
|
||||
:basic_info
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do |e|
|
||||
flash[:error] = "sorry, couldn't find that object... (404)"
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
60
app/controllers/backend/application_controller.rb
Normal file
60
app/controllers/backend/application_controller.rb
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
module Backend
|
||||
class ApplicationController < ActionController::Base
|
||||
include PublicActivity::StoreController
|
||||
include Pundit::Authorization
|
||||
|
||||
layout "backend"
|
||||
|
||||
after_action :verify_authorized
|
||||
|
||||
helper_method :current_user, :user_signed_in?
|
||||
|
||||
before_action :authenticate_user!, :set_honeybadger_context
|
||||
|
||||
before_action :set_paper_trail_whodunnit
|
||||
|
||||
def current_user
|
||||
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
|
||||
end
|
||||
|
||||
def current_impersonator
|
||||
@current_impersonator ||= User.find_by(id: session[:impersonator_user_id]) if session[:impersonator_user_id]
|
||||
end
|
||||
|
||||
alias_method :find_current_auditor, :current_user
|
||||
alias_method :user_for_public_activity, :current_user
|
||||
|
||||
def user_for_paper_trail = current_impersonator&.id || current_user&.id
|
||||
def info_for_paper_trail = { extra_data: { ip: request.remote_ip, user_agent: request.user_agent, impersonating: !!current_impersonator, pretending_to_be: current_impersonator && current_user }.compact_blank }
|
||||
|
||||
def user_signed_in? = !!current_user
|
||||
|
||||
def authenticate_user!
|
||||
unless user_signed_in?
|
||||
return redirect_to backend_login_path, alert: ("you need to be logged in!")
|
||||
end
|
||||
unless @current_user&.active?
|
||||
session[:user_id] = nil
|
||||
@current_user = nil
|
||||
redirect_to backend_login_path, alert: ("you need to be logged in!")
|
||||
end
|
||||
end
|
||||
|
||||
def set_honeybadger_context
|
||||
Honeybadger.context({
|
||||
user_id: current_user&.id,
|
||||
user_username: current_user&.username
|
||||
})
|
||||
end
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError do |e|
|
||||
flash[:error] = "you don't seem to be authorized to do that?"
|
||||
redirect_to backend_root_path
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do |e|
|
||||
flash[:error] = "sorry, couldn't find that object... (404)"
|
||||
redirect_to backend_root_path
|
||||
end
|
||||
end
|
||||
end
|
||||
12
app/controllers/backend/audit_logs_controller.rb
Normal file
12
app/controllers/backend/audit_logs_controller.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
module Backend
|
||||
class AuditLogsController < ApplicationController
|
||||
skip_after_action :verify_authorized
|
||||
|
||||
def index
|
||||
scope = PublicActivity::Activity.order(created_at: :desc)
|
||||
scope = scope.where(owner_type: "Backend::User") if params[:admin_actions_only]
|
||||
|
||||
@activities = scope.page(params[:page]).per(50)
|
||||
end
|
||||
end
|
||||
end
|
||||
52
app/controllers/backend/break_glass_controller.rb
Normal file
52
app/controllers/backend/break_glass_controller.rb
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Backend::BreakGlassController < Backend::ApplicationController
|
||||
def create
|
||||
@break_glassable = find_break_glassable
|
||||
|
||||
authorize BreakGlassRecord
|
||||
|
||||
break_glass_record = BreakGlassRecord.new(
|
||||
backend_user: current_user,
|
||||
break_glassable: @break_glassable,
|
||||
reason: params[:reason],
|
||||
accessed_at: Time.current,
|
||||
)
|
||||
|
||||
if break_glass_record.save
|
||||
redirect_back(fallback_location: backend_root_path, notice: "Access granted. #{document_type.capitalize} is now visible.")
|
||||
else
|
||||
redirect_back(fallback_location: backend_root_path, alert: "Failed to grant access: #{break_glass_record.errors.full_messages.join(", ")}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# it'd be neat if this was polymorphic
|
||||
def find_break_glassable
|
||||
case params[:break_glassable_type]
|
||||
when "Identity::Document"
|
||||
Identity::Document.find(params[:break_glassable_id])
|
||||
when "Identity::AadhaarRecord"
|
||||
Identity::AadhaarRecord.find(params[:break_glassable_id])
|
||||
when "Identity"
|
||||
Identity.find_by_public_id!(params[:break_glassable_id])
|
||||
else
|
||||
raise ArgumentError, "Invalid break_glassable_type: #{params[:break_glassable_type]}"
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: these should be model methods! @break_glassable.try(:thing_name) || "item"
|
||||
def document_type
|
||||
case @break_glassable.class.name
|
||||
when "Identity::Document"
|
||||
"document"
|
||||
when "Identity::AadhaarRecord"
|
||||
"aadhaar record"
|
||||
when "Identity"
|
||||
"identity"
|
||||
else
|
||||
"item"
|
||||
end
|
||||
end
|
||||
end
|
||||
95
app/controllers/backend/dashboard_controller.rb
Normal file
95
app/controllers/backend/dashboard_controller.rb
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Backend
|
||||
class DashboardController < ApplicationController
|
||||
# i really hope any of this math is right!
|
||||
|
||||
def show
|
||||
authorize Verification
|
||||
|
||||
@time_period = params[:time_period] || "this_month"
|
||||
@start_date = case @time_period
|
||||
when "today"
|
||||
Time.current.beginning_of_day
|
||||
when "this_month"
|
||||
Time.current.beginning_of_month
|
||||
when "all_time"
|
||||
Time.at(0)
|
||||
end
|
||||
|
||||
@verifications = Verification.not_ignored.where("created_at >= ?", @start_date)
|
||||
|
||||
@stats = {
|
||||
total: @verifications.count,
|
||||
approved: @verifications.approved.count,
|
||||
rejected: @verifications.rejected.count,
|
||||
pending: @verifications.pending.count,
|
||||
average_hangtime: calculate_average_hangtime(@verifications)
|
||||
}
|
||||
|
||||
# Calculate rejection reason breakdown
|
||||
@rejection_breakdown = calculate_rejection_breakdown(@verifications.rejected)
|
||||
|
||||
# Get leaderboard data
|
||||
activity_counts = PublicActivity::Activity
|
||||
.where(key: [ "verification.approve", "verification.reject" ])
|
||||
.where("activities.created_at >= ?", @start_date)
|
||||
.where.not(owner_id: nil)
|
||||
.group(:owner_id)
|
||||
.count
|
||||
.sort_by { |_, count| -count }
|
||||
|
||||
user_ids = activity_counts.map(&:first)
|
||||
users = Backend::User.where(id: user_ids).index_by(&:id)
|
||||
|
||||
@leaderboard = activity_counts.map do |user_id, count|
|
||||
{
|
||||
user: users[user_id],
|
||||
processed_count: count
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def calculate_average_hangtime(verifications)
|
||||
return "0 seconds" if verifications.empty?
|
||||
|
||||
total_seconds = verifications.sum do |verification|
|
||||
start_time = verification.pending_at || verification.created_at
|
||||
end_time = verification.approved_at || verification.rejected_at || (verification.updated_at if verification.approved? || verification.rejected?) || Time.now
|
||||
|
||||
end_time - start_time
|
||||
end
|
||||
|
||||
total_seconds / verifications.count
|
||||
end
|
||||
|
||||
# this should be less bad
|
||||
def calculate_rejection_breakdown(rejected_verifications)
|
||||
return {} if rejected_verifications.empty?
|
||||
|
||||
breakdown = {}
|
||||
|
||||
rejected_verifications.each do |verification|
|
||||
next unless verification.rejection_reason.present?
|
||||
|
||||
is_fatal = case verification.class.name
|
||||
when "Verification::DocumentVerification"
|
||||
Verification::DocumentVerification::FATAL_REJECTION_REASONS.include?(verification.rejection_reason)
|
||||
when "Verification::AadhaarVerification"
|
||||
Verification::AadhaarVerification::FATAL_REJECTION_REASONS.include?(verification.rejection_reason)
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
reason_name = verification.try(:rejection_reason_name) || verification.rejection_reason.humanize
|
||||
|
||||
breakdown[reason_name] ||= { count: 0, fatal: is_fatal }
|
||||
breakdown[reason_name][:count] += 1
|
||||
end
|
||||
|
||||
breakdown.sort_by { |_, data| -data[:count] }.to_h
|
||||
end
|
||||
end
|
||||
end
|
||||
132
app/controllers/backend/identities_controller.rb
Normal file
132
app/controllers/backend/identities_controller.rb
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
module Backend
|
||||
class IdentitiesController < ApplicationController
|
||||
before_action :set_identity, except: [ :index ]
|
||||
|
||||
def index
|
||||
authorize Identity
|
||||
|
||||
if (search = params[:search])&.start_with? "ident!"
|
||||
ident = Identity.find_by_public_id(search)
|
||||
return redirect_to backend_identity_path ident if ident.present?
|
||||
end
|
||||
|
||||
@identities = policy_scope(Identity)
|
||||
.search(search&.sub("mailto:", ""))
|
||||
.order(created_at: :desc)
|
||||
.page(params[:page])
|
||||
.per(25)
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @identity
|
||||
|
||||
if current_user.super_admin? || current_user.manual_document_verifier?
|
||||
@available_scopes = [ "basic_info", "legal_name", "address" ]
|
||||
elsif current_user.organized_programs.any?
|
||||
organized_program_ids = current_user.organized_programs.pluck(:id)
|
||||
|
||||
granted_tokens = @identity.access_tokens.where(application_id: organized_program_ids)
|
||||
|
||||
@available_scopes = granted_tokens
|
||||
.map { |token| token.scopes }
|
||||
.flatten
|
||||
.uniq
|
||||
.reject(&:blank?)
|
||||
else
|
||||
@available_scopes = [ "basic_info" ]
|
||||
end
|
||||
|
||||
@verifications = @identity.verifications.includes(:identity_document).order(created_at: :desc)
|
||||
|
||||
@addresses = @identity.addresses.order(created_at: :desc)
|
||||
|
||||
@all_programs = @identity.all_programs.distinct
|
||||
|
||||
identity_activities = @identity.activities.includes(:owner)
|
||||
|
||||
verification_activities = PublicActivity::Activity
|
||||
.where(trackable_type: "Verification", trackable_id: @identity.verifications.pluck(:id))
|
||||
.includes(:trackable, :owner)
|
||||
|
||||
document_ids = @identity.documents.pluck(:id)
|
||||
break_glass_record_ids = BreakGlassRecord.where(break_glassable_type: "Identity::Document", break_glassable_id: document_ids).pluck(:id)
|
||||
break_glass_activities = PublicActivity::Activity
|
||||
.where(trackable_type: "BreakGlassRecord", trackable_id: break_glass_record_ids)
|
||||
.includes(:trackable, :owner)
|
||||
|
||||
@activities = (identity_activities + verification_activities + break_glass_activities)
|
||||
.sort_by(&:created_at).reverse
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @identity, :edit?
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @identity, :update?
|
||||
|
||||
if params[:reason].blank?
|
||||
flash[:alert] = "Reason is required for identity updates"
|
||||
render :edit and return
|
||||
end
|
||||
|
||||
if @identity.update(identity_params)
|
||||
@identity.create_activity(
|
||||
:admin_update,
|
||||
owner: current_user,
|
||||
parameters: {
|
||||
reason: params[:reason],
|
||||
changed_fields: @identity.previous_changes.except("updated_at").keys
|
||||
},
|
||||
)
|
||||
|
||||
flash[:notice] = "Identity updated successfully"
|
||||
redirect_to backend_identity_path(@identity)
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def clear_slack_id
|
||||
authorize @identity
|
||||
|
||||
@identity.update!(slack_id: nil)
|
||||
@identity.create_activity(
|
||||
:clear_slack_id,
|
||||
owner: current_user,
|
||||
)
|
||||
flash[:notice] = "Slack ID cleared."
|
||||
redirect_to backend_identity_path(@identity)
|
||||
end
|
||||
|
||||
def new_vouch
|
||||
authorize Verification::VouchVerification, :create?
|
||||
@vouch = @identity.vouch_verifications.build
|
||||
end
|
||||
|
||||
def create_vouch
|
||||
authorize Verification::VouchVerification, :create?
|
||||
@vouch = @identity.vouch_verifications.build(vouch_params)
|
||||
if @vouch.save
|
||||
flash[:notice] = "Vouch verification created successfully"
|
||||
redirect_to backend_identity_path(@identity)
|
||||
else
|
||||
render :new_vouch
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_identity
|
||||
@identity = policy_scope(Identity).find_by_public_id!(params[:id])
|
||||
end
|
||||
|
||||
def identity_params
|
||||
params.require(:identity).permit(:first_name, :last_name, :legal_first_name, :legal_last_name, :primary_email, :phone_number, :birthday, :country, :hq_override, :ysws_eligible, :permabanned)
|
||||
end
|
||||
|
||||
def vouch_params
|
||||
params.require(:verification_vouch_verification).permit(:evidence)
|
||||
end
|
||||
end
|
||||
end
|
||||
5
app/controllers/backend/no_auth_controller.rb
Normal file
5
app/controllers/backend/no_auth_controller.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module Backend
|
||||
class NoAuthController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
end
|
||||
end
|
||||
77
app/controllers/backend/programs_controller.rb
Normal file
77
app/controllers/backend/programs_controller.rb
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
class Backend::ProgramsController < Backend::ApplicationController
|
||||
before_action :set_program, only: [ :show, :edit, :update, :destroy ]
|
||||
|
||||
def index
|
||||
authorize Program
|
||||
@programs = policy_scope(Program).includes(:identities).order(:name)
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @program
|
||||
@identities_count = @program.identities.distinct.count
|
||||
end
|
||||
|
||||
def new
|
||||
@program = Program.new
|
||||
authorize @program
|
||||
end
|
||||
|
||||
def create
|
||||
@program = Program.new(program_params)
|
||||
authorize @program
|
||||
|
||||
if params[:oauth_application] && params[:oauth_application][:redirect_uri].present?
|
||||
@program.redirect_uri = params[:oauth_application][:redirect_uri]
|
||||
end
|
||||
|
||||
if @program.save
|
||||
redirect_to backend_program_path(@program), notice: "Program was successfully created."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @program
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @program
|
||||
|
||||
if params[:oauth_application] && params[:oauth_application][:redirect_uri].present?
|
||||
@program.redirect_uri = params[:oauth_application][:redirect_uri]
|
||||
end
|
||||
|
||||
if @program.update(program_params_for_user)
|
||||
redirect_to backend_program_path(@program), notice: "Program was successfully updated."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @program
|
||||
@program.destroy
|
||||
redirect_to backend_programs_path, notice: "Program was successfully deleted."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_program
|
||||
@program = Program.find(params[:id])
|
||||
end
|
||||
|
||||
def program_params
|
||||
params.require(:program).permit(:name, :description, :active, scopes_array: [])
|
||||
end
|
||||
|
||||
def program_params_for_user
|
||||
permitted_params = [ :name, :redirect_uri ]
|
||||
|
||||
if policy(@program).update_scopes?
|
||||
permitted_params += [ :description, :active, scopes_array: [] ]
|
||||
end
|
||||
|
||||
params.require(:program).permit(permitted_params)
|
||||
end
|
||||
end
|
||||
82
app/controllers/backend/sessions_controller.rb
Normal file
82
app/controllers/backend/sessions_controller.rb
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
module Backend
|
||||
class SessionsController < ApplicationController
|
||||
skip_before_action :authenticate_user!, only: [ :new, :create, :fake_slack_callback_for_dev ]
|
||||
|
||||
skip_after_action :verify_authorized
|
||||
|
||||
def new
|
||||
redirect_uri = url_for(action: :create, only_path: false)
|
||||
redirect_to User.authorize_url(redirect_uri),
|
||||
host: "https://slack.com",
|
||||
allow_other_host: true
|
||||
end
|
||||
|
||||
def create
|
||||
redirect_uri = url_for(action: :create, only_path: false)
|
||||
|
||||
if params[:error].present?
|
||||
uuid = Honeybadger.notify("Slack OAuth error: #{params[:error]}")
|
||||
redirect_to backend_login_path, alert: "failed to authenticate with Slack! (error: #{uuid})"
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
@user = User.from_slack_token(params[:code], redirect_uri)
|
||||
rescue => e
|
||||
uuid = Honeybadger.notify(e)
|
||||
redirect_to backend_login_path, alert: "error authenticating! (error: #{uuid})"
|
||||
return
|
||||
end
|
||||
|
||||
if @user&.persisted?
|
||||
session[:user_id] = @user.id
|
||||
flash[:success] = "welcome aboard!"
|
||||
redirect_to backend_root_path
|
||||
else
|
||||
redirect_to backend_login_path, alert: "you haven't been provisioned an account on this service yet – this attempt been logged."
|
||||
end
|
||||
end
|
||||
|
||||
def fake_slack_callback_for_dev
|
||||
unless Rails.env.development?
|
||||
Honeybadger.notify("Fake Slack callback attempted in non-development environment. WTF?!")
|
||||
redirect_to backend_root_path, alert: "this is only available in development mode."
|
||||
return
|
||||
end
|
||||
|
||||
@user = User.find_by(slack_id: params[:slack_id], active: true)
|
||||
if @user.nil?
|
||||
redirect_to backend_root_path, alert: "dunno who that is, sorry."
|
||||
return
|
||||
end
|
||||
|
||||
session[:user_id] = @user.id
|
||||
redirect_to backend_root_path, notice: "welcome aboard!"
|
||||
end
|
||||
|
||||
def impersonate
|
||||
unless current_user.superadmin?
|
||||
redirect_to backend_root_path, alert: "you are not authorized to impersonate users. this incident has been reported :-P"
|
||||
Honeybadger.notify("Impersonation attempt by #{current_user.username} to #{params[:id]}")
|
||||
return
|
||||
end
|
||||
|
||||
session[:impersonator_user_id] ||= current_user.id
|
||||
user = User.find(params[:id])
|
||||
session[:user_id] = user.id
|
||||
flash[:success] = "hey #{user.username}! how's it going? nice 'stache and glasses!"
|
||||
redirect_to backend_root_path
|
||||
end
|
||||
|
||||
def stop_impersonating
|
||||
session[:user_id] = session[:impersonator_user_id]
|
||||
session[:impersonator_user_id] = nil
|
||||
redirect_to backend_root_path, notice: "welcome back, 007!"
|
||||
end
|
||||
|
||||
def destroy
|
||||
session[:user_id] = nil
|
||||
redirect_to backend_root_path, notice: "bye, see you next time!"
|
||||
end
|
||||
end
|
||||
end
|
||||
19
app/controllers/backend/static_pages_controller.rb
Normal file
19
app/controllers/backend/static_pages_controller.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
module Backend
|
||||
class StaticPagesController < ApplicationController
|
||||
skip_before_action :authenticate_user!, only: [ :login ]
|
||||
skip_after_action :verify_authorized
|
||||
|
||||
def index
|
||||
if current_user&.manual_document_verifier? || current_user&.super_admin?
|
||||
@pending_verifications_count = Verification.where(status: "pending").count
|
||||
end
|
||||
end
|
||||
|
||||
def login
|
||||
end
|
||||
|
||||
def session_dump
|
||||
raise "can't do that!" if Rails.env.production?
|
||||
end
|
||||
end
|
||||
end
|
||||
73
app/controllers/backend/users_controller.rb
Normal file
73
app/controllers/backend/users_controller.rb
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
module Backend
|
||||
class UsersController < ApplicationController
|
||||
before_action :set_user, except: [ :index, :new, :create ]
|
||||
|
||||
def index
|
||||
authorize Backend::User
|
||||
@users = User.all
|
||||
end
|
||||
|
||||
def new
|
||||
authorize User
|
||||
@user = User.new
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @user
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @user
|
||||
@user.update!(user_params)
|
||||
redirect_to backend_users_path, notice: "User updated!"
|
||||
rescue => e
|
||||
redirect_to backend_users_path, alert: e.message
|
||||
end
|
||||
|
||||
def create
|
||||
authorize User
|
||||
@user = User.new(new_user_params.merge(active: true))
|
||||
if @user.save
|
||||
redirect_to backend_users_path, notice: "User created!"
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @user
|
||||
end
|
||||
|
||||
def activate
|
||||
authorize @user
|
||||
@user.activate!
|
||||
flash[:success] = "User activated!"
|
||||
redirect_to @user
|
||||
end
|
||||
|
||||
def deactivate
|
||||
authorize @user
|
||||
if @user == current_user
|
||||
flash[:warning] = "i'm not sure that's a great idea..."
|
||||
return redirect_to @user
|
||||
end
|
||||
@user.deactivate!
|
||||
flash[:success] = "User deactivated."
|
||||
redirect_to @user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:backend_user).permit(:username, :icon_url, :all_fields_access, :human_endorser, :program_manager, :manual_document_verifier, :super_admin, organized_program_ids: [])
|
||||
end
|
||||
|
||||
def new_user_params
|
||||
params.require(:backend_user).permit(:slack_id, :username, :all_fields_access, :human_endorser, :program_manager, :manual_document_verifier, :super_admin, organized_program_ids: [])
|
||||
end
|
||||
end
|
||||
end
|
||||
131
app/controllers/backend/verifications_controller.rb
Normal file
131
app/controllers/backend/verifications_controller.rb
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
module Backend
|
||||
class VerificationsController < ApplicationController
|
||||
before_action :set_verification, only: [ :show, :approve, :reject, :ignore ]
|
||||
|
||||
def index
|
||||
authorize Verification
|
||||
@recent_verifications = Verification.includes(:identity, :identity_document)
|
||||
.where.not(status: "pending")
|
||||
.order(updated_at: :desc)
|
||||
.page(params[:page])
|
||||
.per(20)
|
||||
end
|
||||
|
||||
def pending
|
||||
authorize Verification
|
||||
@pending_verifications = Verification.includes(:identity, :identity_document, identity_document: { files_attachments: :blob })
|
||||
.where(status: "pending")
|
||||
.order(created_at: :asc)
|
||||
.page(params[:page])
|
||||
.per(20)
|
||||
@average_hangtime = @pending_verifications.average("EXTRACT(EPOCH FROM (NOW() - COALESCE(verifications.pending_at, verifications.created_at)))").to_i if @pending_verifications.any?
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @verification
|
||||
|
||||
# Fetch verification activities
|
||||
verification_activities = @verification.activities.includes(:owner)
|
||||
|
||||
# Fetch break glass activities efficiently with a single query
|
||||
break_glass_activities = []
|
||||
unless @verification.is_a?(Verification::VouchVerification)
|
||||
@relevant_object = @verification.identity_document || @verification.aadhaar_record
|
||||
break_glass_record_ids = @relevant_object&.break_glass_records&.pluck(:id) || []
|
||||
break_glass_activities = PublicActivity::Activity
|
||||
.where(trackable_type: "BreakGlassRecord", trackable_id: break_glass_record_ids)
|
||||
.includes(:trackable, :owner)
|
||||
end
|
||||
@activities = (verification_activities + break_glass_activities).sort_by(&:created_at).reverse
|
||||
end
|
||||
|
||||
def approve
|
||||
authorize @verification, :approve?
|
||||
|
||||
@verification.approve!
|
||||
|
||||
# Set YSWS eligibility if provided
|
||||
if params[:ysws_eligible].present?
|
||||
ysws_eligible = params[:ysws_eligible] == "true"
|
||||
@verification.identity.update!(ysws_eligible: ysws_eligible)
|
||||
|
||||
# Send appropriate mailer based on YSWS eligibility and adult program status
|
||||
if ysws_eligible || @verification.identity.came_in_through_adult_program
|
||||
VerificationMailer.approved(@verification).deliver_now
|
||||
else
|
||||
IdentityMailer.approved_but_ysws_ineligible(@verification.identity).deliver_now
|
||||
Slack::NotifyGuardiansJob.perform_later(@verification.identity)
|
||||
end
|
||||
|
||||
eligibility_text = ysws_eligible ? "YSWS eligible" : "YSWS ineligible"
|
||||
flash[:success] = "Document approved and marked as #{eligibility_text}!"
|
||||
else
|
||||
VerificationMailer.approved(@verification).deliver_now
|
||||
flash[:success] = "Document approved successfully!"
|
||||
end
|
||||
|
||||
@verification.create_activity(key: "verification.approve", owner: current_user, parameters: { ysws_eligible: ysws_eligible })
|
||||
|
||||
redirect_to pending_backend_verifications_path
|
||||
end
|
||||
|
||||
def reject
|
||||
authorize @verification, :reject?
|
||||
|
||||
reason = params[:rejection_reason]
|
||||
details = params[:rejection_reason_details]
|
||||
internal_comment = params[:internal_rejection_comment]
|
||||
|
||||
if reason.blank?
|
||||
flash[:error] = "Rejection reason is required"
|
||||
redirect_to backend_verification_path(@verification)
|
||||
return
|
||||
end
|
||||
|
||||
@verification.mark_as_rejected!(reason, details)
|
||||
@verification.internal_rejection_comment = internal_comment if internal_comment.present?
|
||||
@verification.save!
|
||||
|
||||
@verification.create_activity(key: "verification.reject", owner: current_user, parameters: { reason: reason, details: details, internal_comment: internal_comment })
|
||||
|
||||
flash[:success] = "Document rejected with feedback"
|
||||
redirect_to pending_backend_verifications_path
|
||||
end
|
||||
|
||||
def ignore
|
||||
authorize @verification, :ignore?
|
||||
|
||||
if params[:reason].blank?
|
||||
flash[:alert] = "Reason is required to ignore verification"
|
||||
redirect_to backend_verification_path(@verification) and return
|
||||
end
|
||||
|
||||
@verification.update!(
|
||||
ignored_at: Time.current,
|
||||
ignored_reason: params[:reason],
|
||||
)
|
||||
|
||||
@verification.create_activity(
|
||||
:ignored,
|
||||
owner: current_user,
|
||||
parameters: { reason: params[:reason] },
|
||||
)
|
||||
|
||||
flash[:notice] = "Verification ignored successfully"
|
||||
redirect_to backend_identity_path(@verification.identity)
|
||||
end
|
||||
|
||||
rescue_from AASM::InvalidTransition, with: :oops
|
||||
|
||||
private
|
||||
|
||||
def set_verification
|
||||
@verification = Verification.includes(:identity, identity_document: :break_glass_records).find_by_public_id!(params[:id])
|
||||
end
|
||||
|
||||
def oops
|
||||
flash[:warning] = "This verification has already been processed?"
|
||||
redirect_to pending_backend_verifications_path
|
||||
end
|
||||
end
|
||||
end
|
||||
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
34
app/controllers/concerns/is_sneaky.rb
Normal file
34
app/controllers/concerns/is_sneaky.rb
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
module IsSneaky
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def hide_some_data_away
|
||||
if params[:stash_data]
|
||||
stash = begin
|
||||
b64ed = Base64.urlsafe_decode64(params[:stash_data]).force_encoding("UTF-8")
|
||||
unlzed = LZString::UTF16.decompress(b64ed)
|
||||
JSON.parse(unlzed)
|
||||
rescue StandardError
|
||||
{}
|
||||
end
|
||||
request.reset_session if stash["invalidate_session"]
|
||||
session[:stashed_data] = stash
|
||||
end
|
||||
end
|
||||
|
||||
def safe_redirect_url(key)
|
||||
return unless session[:stashed_data]&.[](key)
|
||||
redirect_url = session[:stashed_data][key]
|
||||
redirect_domain = URI.parse(redirect_url).host rescue nil
|
||||
return unless redirect_domain
|
||||
allowed_domains = Program.pluck(:redirect_uri).flat_map { |uri|
|
||||
uri.split("\n").map { |u| URI.parse(u).host rescue nil }
|
||||
}.compact.uniq
|
||||
allowed_domains << "localhost" unless Rails.env.production?
|
||||
|
||||
if allowed_domains.include?(redirect_domain)
|
||||
redirect_url
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
245
app/controllers/onboardings_controller.rb
Normal file
245
app/controllers/onboardings_controller.rb
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
# HERE BE DRAGONS.
|
||||
# this controller sucks!
|
||||
# replace this with zombocom/wicked or something, this is a terrible way to do a wizard
|
||||
class OnboardingsController < ApplicationController
|
||||
skip_before_action :authenticate_identity!, only: [ :show, :welcome, :signin, :basic_info, :create_basic_info ]
|
||||
before_action :ensure_correct_step, except: [ :show, :create_basic_info, :create_document, :submit_aadhaar, :address, :create_address, :signin, :continue, :submitted ]
|
||||
before_action :set_identity, except: [ :show, :welcome, :signin, :basic_info, :create_basic_info ]
|
||||
before_action :ensure_aadhaar_makes_sense, only: [ :aadhaar, :submit_aadhaar, :aadhaar_step_2 ]
|
||||
|
||||
ONBOARDING_STEPS = %w[welcome basic_info document aadhaar address submitted].freeze
|
||||
|
||||
def show
|
||||
redirect_to determine_current_step
|
||||
end
|
||||
|
||||
def welcome
|
||||
end
|
||||
|
||||
def signin
|
||||
flash[:warning] = nil
|
||||
redirect_to new_sessions_path
|
||||
end
|
||||
|
||||
def basic_info
|
||||
@identity = current_identity || Identity.new
|
||||
end
|
||||
|
||||
def create_basic_info
|
||||
return redirect_to_current_step if current_identity&.persisted?
|
||||
params[:identity]&.[](:primary_email)&.downcase!
|
||||
|
||||
existing_identity = Identity.find_by(primary_email: params.dig(:identity, :primary_email))
|
||||
if existing_identity
|
||||
session[:sign_in_email] = existing_identity.primary_email
|
||||
flash[:warning] = "An account with this email already exists. <a href='#{signin_onboarding_path}'>Sign in here</a> if it's yours.".html_safe
|
||||
@identity = Identity.new(basic_info_params)
|
||||
render :basic_info, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
@identity = Identity.new(basic_info_params)
|
||||
|
||||
if @identity.save
|
||||
session[:identity_id] = @identity.id
|
||||
redirect_to_current_step
|
||||
else
|
||||
render :basic_info, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def document
|
||||
if @identity.verification_status == "ineligible"
|
||||
redirect_to submitted_onboarding_path and return
|
||||
end
|
||||
|
||||
@document = @identity.documents.build
|
||||
@is_resubmission = resubmission_scenario?
|
||||
@rejected_verifications = rejected_verifications_for_resubmission if @is_resubmission
|
||||
end
|
||||
|
||||
def create_document
|
||||
return redirect_to_basic_info_onboarding_path unless @identity
|
||||
|
||||
if @identity.verification_status == "ineligible"
|
||||
redirect_to submitted_onboarding_path and return
|
||||
end
|
||||
|
||||
@document = @identity.documents.build(document_params)
|
||||
|
||||
if create_document_and_verification
|
||||
VerificationMailer.created(@verification).deliver_later
|
||||
Identity::NoticeResemblancesJob.perform_later(@identity)
|
||||
redirect_to_current_step
|
||||
else
|
||||
@is_resubmission = resubmission_scenario?
|
||||
@rejected_verifications = rejected_verifications_for_resubmission if @is_resubmission
|
||||
set_default_document_type
|
||||
render :document, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def aadhaar
|
||||
if @identity.verification_status == "ineligible"
|
||||
redirect_to submitted_onboarding_path and return
|
||||
end
|
||||
end
|
||||
|
||||
def submit_aadhaar
|
||||
if @identity.verification_status == "ineligible"
|
||||
redirect_to submitted_onboarding_path and return
|
||||
end
|
||||
|
||||
if aadhaar_params[:aadhaar_number].present? && Identity.where(aadhaar_number: aadhaar_params[:aadhaar_number]).where.not(id: current_identity.id).exists?
|
||||
flash[:warning] = "An account with this Aadhaar number already exists. <a href='#{signin_onboarding_path}'>Sign in here</a> if it's yours.".html_safe
|
||||
render :aadhaar, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
Rails.logger.info "Updating identity with Aadhaar number: #{aadhaar_params.inspect}"
|
||||
|
||||
begin
|
||||
@identity.update!(aadhaar_params)
|
||||
@aadhaar_verification = @identity.aadhaar_verifications.create!
|
||||
redirect_to_current_step
|
||||
rescue StandardError => e
|
||||
uuid = Honeybadger.notify(e)
|
||||
Rails.logger.error "Aadhaar update failed with errors: #{e.message} (report error ID: #{uuid})"
|
||||
render :aadhaar, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def aadhaar_step_2
|
||||
if @identity.verification_status == "ineligible"
|
||||
redirect_to submitted_onboarding_path and return
|
||||
end
|
||||
|
||||
@verification = @identity.aadhaar_verifications.draft.first
|
||||
end
|
||||
|
||||
def address
|
||||
@address = @identity.addresses.build
|
||||
@address.first_name = @identity.first_name
|
||||
@address.last_name = @identity.last_name
|
||||
@address.country = @identity.country
|
||||
end
|
||||
|
||||
def create_address
|
||||
return redirect_to_basic_info_onboarding_path unless @identity
|
||||
|
||||
@address = @identity.addresses.build(address_params)
|
||||
|
||||
if @address.save
|
||||
if @identity.primary_address.nil?
|
||||
@identity.update!(primary_address: @address)
|
||||
end
|
||||
redirect_to_current_step
|
||||
else
|
||||
render :address, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def submitted
|
||||
if session[:oauth_return_to]
|
||||
redirect_to continue_onboarding_path
|
||||
return
|
||||
end
|
||||
|
||||
@documents = @identity.documents.includes(:verifications)
|
||||
end
|
||||
|
||||
def continue
|
||||
return_path = session[:oauth_return_to] || root_path
|
||||
session[:oauth_return_to] = nil
|
||||
redirect_to return_path, allow_other_host: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_identity
|
||||
@identity = current_identity
|
||||
redirect_to basic_info_onboarding_path unless @identity&.persisted?
|
||||
end
|
||||
|
||||
def ensure_correct_step
|
||||
correct_step_path = determine_current_step
|
||||
|
||||
return if request.path == correct_step_path
|
||||
|
||||
Rails.logger.info "Onboarding step redirect: #{request.path} -> #{correct_step_path}"
|
||||
redirect_to correct_step_path
|
||||
end
|
||||
|
||||
def redirect_to_current_step
|
||||
redirect_to determine_current_step
|
||||
end
|
||||
|
||||
def ensure_aadhaar_makes_sense
|
||||
redirect_to_current_step unless Flipper.enabled?(:integrated_aadhaar_2025_07_10, @identity) && @identity&.country == "IN"
|
||||
end
|
||||
|
||||
# disgusting disgusting disgusting
|
||||
def determine_current_step
|
||||
identity = current_identity
|
||||
|
||||
unless identity&.persisted?
|
||||
return basic_info_onboarding_path if request.path == basic_info_onboarding_path
|
||||
return welcome_onboarding_path
|
||||
end
|
||||
|
||||
identity.onboarding_redirect_path
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Onboarding step determination failed: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
welcome_onboarding_path
|
||||
end
|
||||
|
||||
def resubmission_scenario? = @identity.in_resubmission_flow?
|
||||
|
||||
def rejected_verifications_for_resubmission = @identity.rejected_verifications_for_context
|
||||
|
||||
def create_document_and_verification
|
||||
return false unless @document.save
|
||||
|
||||
@verification = @identity.document_verifications.build(
|
||||
identity_document: @document,
|
||||
)
|
||||
|
||||
unless @verification.save
|
||||
Rails.logger.error "Verification creation failed: #{@verification.errors.full_messages}"
|
||||
@document.errors.add(:base, "Unable to create verification: #{@verification.errors.full_messages.join(", ")}")
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.error "Document creation failed: #{e.message}"
|
||||
@document.errors.add(:base, "Unable to save document: #{e.message}")
|
||||
false
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Unexpected error creating document: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
Honeybadger.notify(e)
|
||||
@document.errors.add(:base, "An unexpected error occurred. Please try again.")
|
||||
false
|
||||
end
|
||||
|
||||
def set_default_document_type
|
||||
return if @document.document_type.present?
|
||||
|
||||
@document.document_type = Identity::Document.selectable_types_for_country(@identity.country)&.first
|
||||
end
|
||||
|
||||
def basic_info_params
|
||||
params.require(:identity).permit(
|
||||
:first_name, :last_name, :legal_first_name, :legal_last_name,
|
||||
:country, :primary_email, :slack_id, :birthday, :phone_number
|
||||
)
|
||||
end
|
||||
|
||||
def document_params = params.require(:identity_document).permit(:document_type, files: [])
|
||||
|
||||
def aadhaar_params = params.require(:identity).permit(:aadhaar_number)
|
||||
|
||||
def address_params = params.require(:address).permit(:first_name, :last_name, :line_1, :line_2, :city, :state, :postal_code, :country)
|
||||
end
|
||||
116
app/controllers/sessions_controller.rb
Normal file
116
app/controllers/sessions_controller.rb
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
class SessionsController < ApplicationController
|
||||
skip_before_action :authenticate_identity!, only: [ :new, :create, :check_your_email, :verify, :confirm ]
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
params[:email]&.downcase!
|
||||
@identity = Identity.find_by(primary_email: params[:email])
|
||||
|
||||
if @identity
|
||||
return_url = params[:return_url] || session[:oauth_return_to]
|
||||
|
||||
@login_code = Identity::LoginCode.generate(@identity, return_url: return_url)
|
||||
|
||||
if Rails.env.production?
|
||||
IdentityMailer.login_code(@login_code).deliver_later
|
||||
else
|
||||
IdentityMailer.login_code(@login_code).deliver_now
|
||||
end
|
||||
|
||||
redirect_to check_your_email_sessions_path, notice: "Login code sent to #{@identity.primary_email}"
|
||||
else
|
||||
flash[:info] = "we don't seem to have that email on file – let's get you on board!"
|
||||
session[:stashed_data] ||= {}
|
||||
session[:stashed_data]["prefill"] ||= {}
|
||||
session[:stashed_data]["prefill"]["email"] = params[:email]
|
||||
redirect_to basic_info_onboarding_path
|
||||
end
|
||||
end
|
||||
|
||||
def check_your_email
|
||||
end
|
||||
|
||||
def verify
|
||||
token = params[:token]
|
||||
|
||||
unless token
|
||||
redirect_to check_your_email_sessions_path, alert: "No login token provided."
|
||||
return
|
||||
end
|
||||
|
||||
@login_code = Identity::LoginCode.valid.find_by(token: token)
|
||||
|
||||
if @login_code
|
||||
else
|
||||
redirect_to new_sessions_path, alert: "Invalid or expired login link."
|
||||
end
|
||||
end
|
||||
|
||||
def confirm
|
||||
token = params[:token]
|
||||
|
||||
unless token
|
||||
redirect_to new_sessions_path, alert: "No login token provided."
|
||||
return
|
||||
end
|
||||
|
||||
@login_code = Identity::LoginCode.valid.find_by(token: token)
|
||||
|
||||
if @login_code
|
||||
@login_code.mark_used!
|
||||
|
||||
session[:identity_id] = @login_code.identity.id
|
||||
|
||||
redirect_path = determine_redirect_after_login(@login_code)
|
||||
flash[:success] = "You're in!"
|
||||
redirect_to redirect_path
|
||||
else
|
||||
redirect_to new_sessions_path, alert: "Invalid or expired login link."
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
session[:identity_id] = nil
|
||||
redirect_to root_path, notice: "Successfully signed out"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def deliver_login_code(login_code)
|
||||
login_link = verify_sessions_url(token: login_code.token)
|
||||
Rails.logger.info "LOGIN LINK for #{login_code.identity.primary_email}:"
|
||||
Rails.logger.info login_link
|
||||
Rails.logger.info "Token: #{login_code.token}"
|
||||
end
|
||||
|
||||
def determine_redirect_after_login(login_code)
|
||||
if login_code.return_url.present? && safe_return_url?(login_code.return_url)
|
||||
return login_code.return_url
|
||||
end
|
||||
|
||||
identity = login_code.identity
|
||||
|
||||
if identity.verification_status != "verified"
|
||||
determine_onboarding_step(identity)
|
||||
else
|
||||
session[:oauth_return_to] || root_path
|
||||
end
|
||||
end
|
||||
|
||||
def safe_return_url?(url)
|
||||
return false if url.blank?
|
||||
|
||||
begin
|
||||
uri = URI.parse(url)
|
||||
uri.relative? || uri.host == request.host
|
||||
rescue URI::InvalidURIError
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def determine_onboarding_step(identity)
|
||||
identity.onboarding_redirect_path
|
||||
end
|
||||
end
|
||||
37
app/controllers/slack_accounts_controller.rb
Normal file
37
app/controllers/slack_accounts_controller.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
class SlackAccountsController < ApplicationController
|
||||
before_action :authenticate_identity!
|
||||
|
||||
def new
|
||||
redirect_uri = url_for(action: :create, only_path: false)
|
||||
Rails.logger.info "Starting Slack OAuth flow for account linking with redirect URI: #{redirect_uri}"
|
||||
redirect_to Identity.slack_authorize_url(redirect_uri),
|
||||
host: "https://slack.com",
|
||||
allow_other_host: true
|
||||
end
|
||||
|
||||
def create
|
||||
redirect_uri = url_for(action: :create, only_path: false)
|
||||
|
||||
if params[:error].present?
|
||||
Rails.logger.error "Slack OAuth error: #{params[:error]}"
|
||||
uuid = Honeybadger.notify("Slack OAuth error: #{params[:error]}")
|
||||
redirect_to root_path, alert: "failed to link Slack account! (error: #{uuid})"
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
result = Identity.link_slack_account(params[:code], redirect_uri, current_identity)
|
||||
|
||||
if result[:success]
|
||||
Rails.logger.info "Successfully linked Slack account #{result[:slack_id]} to Identity #{current_identity.id}"
|
||||
redirect_to root_path, notice: "Successfully linked your Slack account!"
|
||||
else
|
||||
redirect_to root_path, alert: result[:error]
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "Error linking Slack account: #{e.message}"
|
||||
uuid = Honeybadger.notify(e)
|
||||
redirect_to root_path, alert: "error linking Slack account! (error: #{uuid})"
|
||||
end
|
||||
end
|
||||
end
|
||||
13
app/controllers/static_pages_controller.rb
Normal file
13
app/controllers/static_pages_controller.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
class StaticPagesController < ApplicationController
|
||||
skip_before_action :authenticate_identity!, only: [ :faq, :external_api_docs ]
|
||||
|
||||
def index
|
||||
end
|
||||
|
||||
def faq
|
||||
end
|
||||
|
||||
def external_api_docs
|
||||
render :external_api_docs, layout: "backend"
|
||||
end
|
||||
end
|
||||
73
app/controllers/webhooks/aadhaar_controller.rb
Normal file
73
app/controllers/webhooks/aadhaar_controller.rb
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
module Webhooks
|
||||
class AadhaarController < ApplicationController
|
||||
def create
|
||||
Honeybadger.context({
|
||||
tags: "webhook, aadhaar"
|
||||
})
|
||||
|
||||
unless params[:secret_key] == Rails.application.credentials.aadhaar.webhook_secret
|
||||
Honeybadger.notify("Aadhaar webhook: invalid secret key :-/")
|
||||
return render json: { error: "aw hell nah" }, status: :unauthorized
|
||||
end
|
||||
|
||||
data = params[:response_data]
|
||||
unless data
|
||||
Honeybadger.notify("Aadhaar webhook: invalid data :-O")
|
||||
return render json: { error: "???" }, status: :unauthorized
|
||||
end
|
||||
|
||||
data = Base64.decode64(data)
|
||||
data = JSON.parse(data, symbolize_names: true)
|
||||
|
||||
raise "unknown digilocker status API status!" unless data[:status] == 1
|
||||
|
||||
data[:data].each_pair do |tx_id, tx_data|
|
||||
aadhaar_verification = Verification::AadhaarVerification.find_by(aadhaar_external_transaction_id: tx_id)
|
||||
|
||||
unless aadhaar_verification
|
||||
Honeybadger.notify("Aadhaar webhook: no verification found for tx_id #{tx_id}")
|
||||
next
|
||||
end
|
||||
|
||||
if tx_data[:final_status] == "Denied"
|
||||
aadhaar_verification.mark_as_rejected!("service_unavailable", "Verification denied by Aadhaar service")
|
||||
next
|
||||
end
|
||||
|
||||
unless tx_data[:final_status] == "Completed"
|
||||
Honeybadger.notify("Aadhaar webhook: verification #{tx_id} not completed")
|
||||
next
|
||||
end
|
||||
|
||||
aadhaar_doc = tx_data[:msg].first { |doc| doc[:doc_type] == "ADHAR" }
|
||||
|
||||
unless aadhaar_doc
|
||||
Honeybadger.notify("Aadhaar webhook: no Aadhaar document found for tx_id #{tx_id}???")
|
||||
next
|
||||
end
|
||||
|
||||
aadhaar_data = aadhaar_doc[:data]
|
||||
|
||||
aadhaar_verification.create_aadhaar_record!(
|
||||
identity: aadhaar_verification.identity,
|
||||
date_of_birth: Date.parse(aadhaar_data[:dob]),
|
||||
name: aadhaar_data[:name],
|
||||
raw_json_response: aadhaar_doc.to_json,
|
||||
)
|
||||
|
||||
aadhaar_verification.create_activity(
|
||||
"data_received",
|
||||
owner: aadhaar_verification.identity,
|
||||
)
|
||||
|
||||
aadhaar_verification.mark_pending!
|
||||
end
|
||||
|
||||
render json: { message: "thanks!" }, status: :ok
|
||||
rescue StandardError => e
|
||||
raise
|
||||
Honeybadger.notify(e)
|
||||
render json: { error: "???" }, status: :internal_server_error
|
||||
end
|
||||
end
|
||||
end
|
||||
4
app/controllers/webhooks/application_controller.rb
Normal file
4
app/controllers/webhooks/application_controller.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module Webhooks
|
||||
class ApplicationController < ActionController::API
|
||||
end
|
||||
end
|
||||
5
app/frontend/entrypoints/application.css
Normal file
5
app/frontend/entrypoints/application.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@import "../stylesheets/application.scss";
|
||||
|
||||
body {
|
||||
font-family: var(--preferred-font), system-ui;
|
||||
}
|
||||
5
app/frontend/entrypoints/application.js
Normal file
5
app/frontend/entrypoints/application.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import "../js/alpine.js";
|
||||
import "../js/lightswitch.js";
|
||||
import "../js/click-to-copy";
|
||||
import htmx from "htmx.org"
|
||||
window.htmx = htmx
|
||||
8
app/frontend/entrypoints/backend.css
Normal file
8
app/frontend/entrypoints/backend.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
@import "../stylesheets/backend.scss";
|
||||
|
||||
@import "../stylesheets/os9.css";
|
||||
@import "../stylesheets/layout.css";
|
||||
|
||||
body {
|
||||
font-family: var(--preferred-font), system-ui;
|
||||
}
|
||||
1
app/frontend/entrypoints/backend.js
Normal file
1
app/frontend/entrypoints/backend.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import "../js/click-to-copy";
|
||||
2
app/frontend/entrypoints/direct_upload.js
Normal file
2
app/frontend/entrypoints/direct_upload.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import * as ActiveStorage from "@rails/activestorage"
|
||||
ActiveStorage.start()
|
||||
0
app/frontend/images/.keep
Normal file
0
app/frontend/images/.keep
Normal file
BIN
app/frontend/images/hc-square.png
Normal file
BIN
app/frontend/images/hc-square.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
app/frontend/images/icons/break-the-glass.png
Normal file
BIN
app/frontend/images/icons/break-the-glass.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
app/frontend/images/loader.gif
Normal file
BIN
app/frontend/images/loader.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
3
app/frontend/js/alpine.js
Normal file
3
app/frontend/js/alpine.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Alpine from 'alpinejs'
|
||||
window.Alpine = Alpine
|
||||
Alpine.start()
|
||||
21
app/frontend/js/click-to-copy.js
Normal file
21
app/frontend/js/click-to-copy.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import $ from "jquery";
|
||||
|
||||
$('[data-copy-to-clipboard]').on('click', async function(e) {
|
||||
const element = e.currentTarget;
|
||||
const textToCopy = element.getAttribute('data-copy-to-clipboard');
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
|
||||
if (element.hasAttribute('aria-label')) {
|
||||
const previousLabel = element.getAttribute('aria-label');
|
||||
element.setAttribute('aria-label', 'copied!');
|
||||
|
||||
setTimeout(() => {
|
||||
element.setAttribute('aria-label', previousLabel);
|
||||
}, 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
});
|
||||
23
app/frontend/js/lightswitch.js
Normal file
23
app/frontend/js/lightswitch.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Get the current theme that was already set in the head
|
||||
const savedTheme = localStorage.getItem("theme") || "light";
|
||||
|
||||
function updateIcon(theme) {
|
||||
const icon = document.querySelector(".lightswitch-icon");
|
||||
if (icon) {
|
||||
icon.textContent = theme === "dark" ? "💡" : "🌙";
|
||||
}
|
||||
}
|
||||
|
||||
// Set initial icon and show button after theme is set
|
||||
updateIcon(savedTheme);
|
||||
const lightswitchBtn = document.getElementById("lightswitch");
|
||||
if (lightswitchBtn) {
|
||||
lightswitchBtn.classList.add("theme-loaded");
|
||||
}
|
||||
|
||||
document.getElementById("lightswitch").addEventListener("click", () => {
|
||||
const theme = document.body.parentElement.dataset.theme === "dark" ? "light" : "dark";
|
||||
document.body.parentElement.dataset.theme = theme;
|
||||
localStorage.setItem("theme", theme);
|
||||
updateIcon(theme);
|
||||
});
|
||||
29
app/frontend/stylesheets/application.scss
Normal file
29
app/frontend/stylesheets/application.scss
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// $theme-color: "amber";
|
||||
$theme-color: "lime";
|
||||
@import "@picocss/pico/scss/pico";
|
||||
|
||||
:root {
|
||||
--pico-font-family-sans-serif: Inter, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif, var(--pico-font-family-emoji) !important;
|
||||
--pico-font-size: 16px !important;
|
||||
/* Original: 100% */
|
||||
--pico-line-height: 1.25 !important;
|
||||
/* Original: 1.5 */
|
||||
--pico-form-element-spacing-vertical: 0.5rem !important;
|
||||
/* Original: 1rem */
|
||||
--pico-form-element-spacing-horizontal: 1.0rem !important;
|
||||
/* Original: 1.25rem */
|
||||
--pico-border-radius: 0.375rem !important;
|
||||
/* Original: 0.25rem */
|
||||
}
|
||||
|
||||
[x-cloak] { display: none !important; }
|
||||
|
||||
@import "./colors.css";
|
||||
@import "./snippets/banners.scss";
|
||||
@import "./snippets/admin_tools.scss";
|
||||
@import "./snippets/forms.scss";
|
||||
@import "./snippets/brand.scss";
|
||||
@import "./snippets/borders.scss";
|
||||
@import "./snippets/lightswitch.scss";
|
||||
@import "./snippets/tooltips.scss";
|
||||
@import "./snippets/footer.scss";
|
||||
36
app/frontend/stylesheets/backend.scss
Normal file
36
app/frontend/stylesheets/backend.scss
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
@import "./colors.css";
|
||||
@import "./snippets/banners.scss";
|
||||
@import "./snippets/admin_tools.scss";
|
||||
@import "./snippets/borders.scss";
|
||||
@import "./snippets/tooltips.scss";
|
||||
|
||||
body {
|
||||
font-size: 12px;
|
||||
margin: 5rem
|
||||
}
|
||||
|
||||
form {
|
||||
grid-template-columns: max-content auto;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.link {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.identity-link {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
ul {
|
||||
list-style: square; list-style-position: inside;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
78
app/frontend/stylesheets/colors.css
Normal file
78
app/frontend/stylesheets/colors.css
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
:root {
|
||||
--error-bg: #fbf2f4;
|
||||
--error-border: #eab4bc;
|
||||
--error-fg: #b9031f;
|
||||
--error-fg-strong: #78202e;
|
||||
|
||||
--warning-bg: #fffcf2;
|
||||
--warning-border: #ffe69b;
|
||||
--warning-fg: #ffc107;
|
||||
--warning-fg-strong: #6a311c;
|
||||
|
||||
--success-bg: #f3f8f5;
|
||||
--success-border: #a1caad;
|
||||
--success-fg: #147b33;
|
||||
--success-fg-strong: #285a37;
|
||||
|
||||
--info-bg: #f2f7fb;
|
||||
--info-bg-selected: #cce0f1;
|
||||
--info-border: #b2d1ea;
|
||||
--info-fg: #0067b9;
|
||||
--info-fg-strong: #1f5077;
|
||||
|
||||
--quote-fg-1: #2b497d;
|
||||
--quote-bg-1: #e8ecf2;
|
||||
--quote-fg-2: #1d3e0f;
|
||||
--quote-bg-2: #e4f1df;
|
||||
--quote-fg-3: #5c0a0a;
|
||||
--quote-bg-3: #f7d4d4;
|
||||
--quote-fg-4: #472215;
|
||||
--quote-bg-4: #dbbeb3;
|
||||
--quote-fg-5: #335f61;
|
||||
--quote-bg-5: #e0e6e6;
|
||||
--purple-bg: #f7f3fb;
|
||||
--purple-border: #c4a3d4;
|
||||
--purple-fg: #7c3aed;
|
||||
--purple-fg-strong: #4c1d95;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--error-bg: #3b1017;
|
||||
--error-border: #8b2535;
|
||||
--error-fg: #dc818f;
|
||||
--error-fg-strong: #e39aa5;
|
||||
|
||||
--warning-bg: #33290b;
|
||||
--warning-border: #7f661c;
|
||||
--warning-fg: #ffe083;
|
||||
--warning-fg-strong: #ffecb4;
|
||||
|
||||
--success-bg: #142c1b;
|
||||
--success-border: #285a37;
|
||||
--success-fg: #8abd99;
|
||||
--success-fg-strong: #b9d8c2;
|
||||
|
||||
--info-bg: #0f273b;
|
||||
--info-bg-selected: #436075;
|
||||
--info-border: #436075;
|
||||
--info-fg: #b4d9f3;
|
||||
--info-fg-strong: #d2e8f7;
|
||||
|
||||
--quote-fg-1: #b3cbff;
|
||||
--quote-bg-1: #373a3f;
|
||||
--quote-fg-2: #bee3aa;
|
||||
--quote-bg-2: #313b2d;
|
||||
--quote-fg-3: #ffc4b3;
|
||||
--quote-bg-3: #55393a;
|
||||
--quote-fg-4: #ffd3c0;
|
||||
--quote-bg-4: #5e473e;
|
||||
--quote-fg-5: #9ac9ca;
|
||||
--quote-bg-5: #393d3e;
|
||||
|
||||
--purple-bg: #2d1b3b;
|
||||
--purple-border: #6b46c1;
|
||||
--purple-fg: #c4b5fd;
|
||||
--purple-fg-strong: #ddd6fe;
|
||||
}
|
||||
}
|
||||
215
app/frontend/stylesheets/layout.css
Normal file
215
app/frontend/stylesheets/layout.css
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
:root {
|
||||
--spacing: 8px;
|
||||
--padding: 8px;
|
||||
--margin: 8px;
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--margin) 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.margin {
|
||||
margin: var(--margin);
|
||||
}
|
||||
|
||||
.margin-top {
|
||||
margin-top: var(--margin);
|
||||
}
|
||||
|
||||
.margin-right {
|
||||
margin-right: var(--margin);
|
||||
}
|
||||
|
||||
.margin-bottom {
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
|
||||
.margin-left {
|
||||
margin-left: var(--margin);
|
||||
}
|
||||
|
||||
.margin-horizontal {
|
||||
margin-left: var(--margin);
|
||||
margin-right: var(--margin);
|
||||
}
|
||||
|
||||
.margin-vertical {
|
||||
margin-top: var(--margin);
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
|
||||
.padding {
|
||||
padding: var(--padding);
|
||||
}
|
||||
|
||||
.padding-top {
|
||||
padding-top: var(--padding);
|
||||
}
|
||||
|
||||
.padding-right {
|
||||
padding-right: var(--padding);
|
||||
}
|
||||
|
||||
.padding-bottom {
|
||||
padding-bottom: var(--padding);
|
||||
}
|
||||
|
||||
.padding-left {
|
||||
padding-left: var(--padding);
|
||||
}
|
||||
|
||||
.padding-horizontal {
|
||||
padding-left: var(--padding);
|
||||
padding-right: var(--padding);
|
||||
}
|
||||
|
||||
.padding-vertical {
|
||||
padding-top: var(--padding);
|
||||
padding-bottom: var(--padding);
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-row.gap,
|
||||
.flex-column.gap {
|
||||
gap: var(--spacing);
|
||||
}
|
||||
|
||||
.flex-row.align-start,
|
||||
.flex-column.align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.flex-row.align-center,
|
||||
.flex-column.align-center,
|
||||
.grid.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-row.align-end,
|
||||
.flex-column.align-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.flex-row.align-stretch,
|
||||
.flex-column.align-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.flex-row.justify-start,
|
||||
.flex-column.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.flex-row.justify-center,
|
||||
.flex-column.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-row.justify-end,
|
||||
.flex-column.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid.gap {
|
||||
grid-column-gap: var(--spacing);
|
||||
grid-row-gap: var(--spacing);
|
||||
}
|
||||
|
||||
.grid.gap-vertical {
|
||||
grid-row-gap: var(--spacing);
|
||||
}
|
||||
|
||||
.grid.gap-horizontal {
|
||||
grid-column-gap: var(--spacing);
|
||||
}
|
||||
|
||||
.checkbox-row, .radio-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-row label, .radio-row label {
|
||||
padding-left: var(--spacing);
|
||||
}
|
||||
|
||||
.window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title-bar-text {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.window-body {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
[role=menu],
|
||||
[role=menubar] {
|
||||
user-select: none;
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[role=menu] {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[role=menu] li,
|
||||
[role=menubar] li {
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
[role=menu] li > ul,
|
||||
[role=menubar] li > ul {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: none;
|
||||
}
|
||||
|
||||
[role=menu] li:focus-within > ul,
|
||||
[role=menubar] li:focus-within > ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.list [role="option"] {
|
||||
user-select: none;
|
||||
}
|
||||
1104
app/frontend/stylesheets/os9.css
Normal file
1104
app/frontend/stylesheets/os9.css
Normal file
File diff suppressed because it is too large
Load diff
41
app/frontend/stylesheets/snippets/admin_tools.scss
Normal file
41
app/frontend/stylesheets/snippets/admin_tools.scss
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
$admin-red: red;
|
||||
$dev-green: #14a514;
|
||||
$mdv-blue: #1e90ff;
|
||||
$tool-font-size: 10px;
|
||||
$tool-border-width: 1.5px;
|
||||
$tool-padding: 2px;
|
||||
$tool-padding-top: 14px;
|
||||
$tool-label-position-top: 0;
|
||||
$tool-label-position-left: 3px;
|
||||
|
||||
@mixin admin-tool($label, $color) {
|
||||
&::before {
|
||||
content: $label;
|
||||
color: $color;
|
||||
position: absolute;
|
||||
font-size: $tool-font-size;
|
||||
top: $tool-label-position-top;
|
||||
left: $tool-label-position-left;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
border: $tool-border-width dashed $color;
|
||||
padding: $tool-padding;
|
||||
padding-top: $tool-padding-top;
|
||||
}
|
||||
|
||||
.super-admin-tool {
|
||||
@include admin-tool("super admin", $admin-red);
|
||||
}
|
||||
|
||||
.mdv-tool {
|
||||
@include admin-tool("manual document verifier", $mdv-blue);
|
||||
}
|
||||
|
||||
.dev-tool {
|
||||
@include admin-tool("development mode", $dev-green);
|
||||
}
|
||||
|
||||
.program-manager-tool {
|
||||
@include admin-tool("program manager", white);
|
||||
}
|
||||
71
app/frontend/stylesheets/snippets/banners.scss
Normal file
71
app/frontend/stylesheets/snippets/banners.scss
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
@mixin banner-base {
|
||||
border: 1px solid;
|
||||
border-radius: 8px;
|
||||
padding: .75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 500;
|
||||
vertical-align: center;
|
||||
|
||||
& > svg {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin banner-theme($bg-color, $border-color, $fg-color, $list-fg-color) {
|
||||
background: $bg-color;
|
||||
border-color: $border-color;
|
||||
color: $fg-color;
|
||||
|
||||
& > ul {
|
||||
color: $list-fg-color;
|
||||
& > li {
|
||||
color: $list-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin banner-warning {
|
||||
@include banner-theme(var(--warning-bg), var(--warning-border), var(--warning-fg-strong), var(--warning-fg));
|
||||
}
|
||||
|
||||
@mixin banner-danger {
|
||||
@include banner-theme(var(--error-bg), var(--error-border), var(--error-fg-strong), var(--error-fg));
|
||||
}
|
||||
|
||||
@mixin banner-info {
|
||||
@include banner-theme(var(--info-bg), var(--info-border), var(--info-fg-strong), var(--info-fg));
|
||||
}
|
||||
|
||||
@mixin banner-success {
|
||||
@include banner-theme(var(--success-bg), var(--success-border), var(--success-fg-strong), var(--success-fg));
|
||||
}
|
||||
|
||||
@mixin banner-purple {
|
||||
@include banner-theme(var(--purple-bg), var(--purple-border), var(--purple-fg-strong), var(--purple-fg));
|
||||
}
|
||||
|
||||
.banner {
|
||||
@include banner-base;
|
||||
|
||||
&.warning {
|
||||
@include banner-warning;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
@include banner-danger;
|
||||
}
|
||||
|
||||
&.info {
|
||||
@include banner-info;
|
||||
}
|
||||
|
||||
&.success {
|
||||
@include banner-success;
|
||||
}
|
||||
|
||||
&.purple {
|
||||
@include banner-purple;
|
||||
}
|
||||
}
|
||||
18
app/frontend/stylesheets/snippets/borders.scss
Normal file
18
app/frontend/stylesheets/snippets/borders.scss
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
.environment {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.staging {
|
||||
border: 5px dashed #ff00c8;
|
||||
}
|
||||
|
||||
&.development {
|
||||
border: 5px dashed #00FF00;
|
||||
}
|
||||
}
|
||||
15
app/frontend/stylesheets/snippets/brand.scss
Normal file
15
app/frontend/stylesheets/snippets/brand.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.brand {
|
||||
&>h1 {
|
||||
display: inline;
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
font-size: 29px;
|
||||
}
|
||||
|
||||
& img {
|
||||
width: 50px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
83
app/frontend/stylesheets/snippets/footer.scss
Normal file
83
app/frontend/stylesheets/snippets/footer.scss
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
@import '@picocss/pico/css/pico.colors.css';
|
||||
|
||||
.app-footer {
|
||||
margin-top: 3rem;
|
||||
padding: 1.5rem 0;
|
||||
border-top: 1px solid var(--pico-muted-border-color);
|
||||
font-size: 0.875rem;
|
||||
color: var(--pico-muted-color);
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.footer-main {
|
||||
.app-name {
|
||||
font-weight: 600;
|
||||
color: var(--pico-color);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-version {
|
||||
text-align: right;
|
||||
|
||||
.version-info {
|
||||
margin-bottom: 0;
|
||||
|
||||
.version-link {
|
||||
color: var(--pico-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-weight: 500;
|
||||
color: var(--pico-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.environment-badge {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
&.development {
|
||||
background-color: var(--pico-color-amber-300);
|
||||
color: var(--pico-color-amber-950);
|
||||
}
|
||||
|
||||
&.staging {
|
||||
background-color: var(--pico-color-purple-500);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.production {
|
||||
background-color: var(--pico-color-green-600);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.footer-version {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
app/frontend/stylesheets/snippets/forms.scss
Normal file
19
app/frontend/stylesheets/snippets/forms.scss
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
.form-one-line {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
label {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
[type="submit"].small {
|
||||
width: fit-content !important;
|
||||
}
|
||||
|
||||
.inline-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
39
app/frontend/stylesheets/snippets/lightswitch.scss
Normal file
39
app/frontend/stylesheets/snippets/lightswitch.scss
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
.lightswitch-btn {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: var(--bg-secondary, #f8f9fa);
|
||||
border: 1px solid var(--border-color, #dee2e6);
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.lightswitch-icon {
|
||||
font-size: 1.2rem;
|
||||
display: block;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&.theme-loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"] .lightswitch-btn {
|
||||
background: var(--bg-secondary, #374151);
|
||||
border-color: var(--border-color, #4b5563);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
84
app/frontend/stylesheets/snippets/tooltips.scss
Normal file
84
app/frontend/stylesheets/snippets/tooltips.scss
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// SHAMELESSLY lifted from HCB
|
||||
|
||||
@use "../colors";
|
||||
|
||||
.tooltipped {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (min-width: 56em) {
|
||||
.tooltipped {
|
||||
&:after {
|
||||
background-color: #ccc;
|
||||
box-shadow:
|
||||
0 0 2px 0 rgba(0, 0, 0, 0.0625),
|
||||
0 4px 8px 0 rgba(0, 0, 0, 0.125);
|
||||
color: var(--window-fg);
|
||||
content: attr(aria-label);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
height: min-content;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.375;
|
||||
max-width: 16rem;
|
||||
min-height: 1.25rem;
|
||||
opacity: 0;
|
||||
padding: 0.15rem 0.55rem;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
text-align: center;
|
||||
transform: translateY(50%);
|
||||
transition: 0.125s all ease-in-out;
|
||||
width: max-content;
|
||||
z-index: 1;
|
||||
|
||||
&.tooltipped--lg {
|
||||
max-width: 24rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:after,
|
||||
&:active:after,
|
||||
&:focus:after {
|
||||
opacity: 1;
|
||||
z-index: 9;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.tooltipped--e:after {
|
||||
left: 100%;
|
||||
bottom: 50%;
|
||||
right: 0;
|
||||
margin-left: 0.5rem;
|
||||
transform: translateY(50%);
|
||||
}
|
||||
|
||||
.tooltipped--w:after {
|
||||
right: 100%;
|
||||
bottom: 50%;
|
||||
margin-right: 0.5rem;
|
||||
transform: translateY(50%);
|
||||
}
|
||||
|
||||
.tooltipped--n:after {
|
||||
right: 50%;
|
||||
bottom: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.tooltipped--s:after {
|
||||
right: 50%;
|
||||
top: 100%;
|
||||
margin-top: 0.5rem;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.tooltipped--xl {
|
||||
&:after {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
app/helpers/addresses_helper.rb
Normal file
2
app/helpers/addresses_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module AddressesHelper
|
||||
end
|
||||
6
app/helpers/api/v1/application_helper.rb
Normal file
6
app/helpers/api/v1/application_helper.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module API::V1::ApplicationHelper
|
||||
def scope(scope, &)
|
||||
return unless current_scopes.include?(scope)
|
||||
yield
|
||||
end
|
||||
end
|
||||
2
app/helpers/api/v1/identities_helper.rb
Normal file
2
app/helpers/api/v1/identities_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module API::V1::IdentitiesHelper
|
||||
end
|
||||
25
app/helpers/application_helper.rb
Normal file
25
app/helpers/application_helper.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
module ApplicationHelper
|
||||
def format_duration(seconds)
|
||||
return "0 seconds" if seconds.nil? || seconds == 0
|
||||
|
||||
hours = seconds / 3600
|
||||
minutes = (seconds % 3600) / 60
|
||||
seconds = seconds % 60
|
||||
|
||||
parts = []
|
||||
parts << "#{hours} #{"hour".pluralize(hours)}" if hours > 0
|
||||
parts << "#{minutes} #{"minute".pluralize(minutes)}" if minutes > 0
|
||||
parts << "#{seconds} #{"second".pluralize(seconds)}" if seconds > 0 || parts.empty?
|
||||
|
||||
parts.join(", ")
|
||||
end
|
||||
|
||||
def copy_to_clipboard(clipboard_value, tooltip_direction: "n", **options, &block)
|
||||
# If block is not given, use clipboard_value as the rendered content
|
||||
block ||= ->(_) { clipboard_value }
|
||||
return yield if options.delete(:if) == false
|
||||
|
||||
css_classes = "pointer tooltipped tooltipped--#{tooltip_direction} #{options.delete(:class)}"
|
||||
tag.span "data-copy-to-clipboard": clipboard_value, class: css_classes, "aria-label": options.delete(:label) || "click to copy...", **options, &block
|
||||
end
|
||||
end
|
||||
30
app/helpers/backend/application_helper.rb
Normal file
30
app/helpers/backend/application_helper.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
module Backend::ApplicationHelper
|
||||
def render_checkbox(value)
|
||||
content_tag(:span, style: "color: var(--checkbox-#{value ? "true" : "false"})") { value ? "☑" : "☒" }
|
||||
end
|
||||
|
||||
def super_admin_tool(class_name: "", element: "div", **options, &block)
|
||||
return unless current_user&.super_admin?
|
||||
concat content_tag(element, class: "super-admin-tool #{class_name}", **options, &block)
|
||||
end
|
||||
|
||||
def break_glass_tool(class_name: "", element: "div", **options, &block)
|
||||
return unless current_user&.can_break_glass? || current_user&.super_admin?
|
||||
concat content_tag(element, class: "break-glass-tool #{class_name}", **options, &block)
|
||||
end
|
||||
|
||||
def program_manager_tool(class_name: "", element: "div", **options, &block)
|
||||
return unless current_user&.program_manager? || current_user&.super_admin?
|
||||
concat content_tag(element, class: "program-manager-tool #{class_name}", **options, &block)
|
||||
end
|
||||
|
||||
def mdv_tool(class_name: "", element: "div", **options, &block)
|
||||
return unless current_user&.manual_document_verifier? || current_user&.super_admin?
|
||||
concat content_tag(element, class: "mdv-tool #{class_name}", **options, &block)
|
||||
end
|
||||
|
||||
def dev_tool(class_name: "", element: "div", **options, &block)
|
||||
return unless Rails.env.development?
|
||||
concat content_tag(element, class: "dev-tool #{class_name}", **options, &block)
|
||||
end
|
||||
end
|
||||
2
app/helpers/backend/audit_logs_helper.rb
Normal file
2
app/helpers/backend/audit_logs_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module Backend::AuditLogsHelper
|
||||
end
|
||||
2
app/helpers/backend/identities_helper.rb
Normal file
2
app/helpers/backend/identities_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module Backend::IdentitiesHelper
|
||||
end
|
||||
2
app/helpers/backend/sessions_helper.rb
Normal file
2
app/helpers/backend/sessions_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module Backend::SessionsHelper
|
||||
end
|
||||
2
app/helpers/backend/users_helper.rb
Normal file
2
app/helpers/backend/users_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module Backend::UsersHelper
|
||||
end
|
||||
2
app/helpers/credentials_helper.rb
Normal file
2
app/helpers/credentials_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module CredentialsHelper
|
||||
end
|
||||
2
app/helpers/onboarding_helper.rb
Normal file
2
app/helpers/onboarding_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module OnboardingHelper
|
||||
end
|
||||
2
app/helpers/static_pages_helper.rb
Normal file
2
app/helpers/static_pages_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module StaticPagesHelper
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue