├── .dockerignore ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── Dockerfile ├── Dockerfile.worker ├── Gemfile ├── Gemfile.lock ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ └── stylesheets │ │ └── application.css ├── components │ ├── api_example.rb │ ├── base.rb │ ├── bootleg_turbo.rb │ ├── brand.rb │ ├── break_the_glass.rb │ ├── break_the_glass_form.rb │ ├── footer.rb │ ├── home_button.rb │ ├── identity.rb │ ├── identity_review │ │ ├── aadhaar_full.rb │ │ ├── aadhaar_info.rb │ │ ├── basic_details.rb │ │ ├── document_files.rb │ │ └── document_info.rb │ ├── inspector.rb │ ├── public_activity │ │ ├── container.rb │ │ └── snippet.rb │ ├── resemblance.rb │ ├── user_mention.rb │ └── window.rb ├── controllers │ ├── aadhaar_controller.rb │ ├── addresses_controller.rb │ ├── api │ │ ├── external │ │ │ ├── application_controller.rb │ │ │ └── identities_controller.rb │ │ └── v1 │ │ │ ├── application_controller.rb │ │ │ ├── hcb_controller.rb │ │ │ ├── health_check_controller.rb │ │ │ └── identities_controller.rb │ ├── application_controller.rb │ ├── backend │ │ ├── application_controller.rb │ │ ├── audit_logs_controller.rb │ │ ├── break_glass_controller.rb │ │ ├── dashboard_controller.rb │ │ ├── identities_controller.rb │ │ ├── no_auth_controller.rb │ │ ├── programs_controller.rb │ │ ├── sessions_controller.rb │ │ ├── static_pages_controller.rb │ │ ├── users_controller.rb │ │ └── verifications_controller.rb │ ├── concerns │ │ ├── .keep │ │ └── is_sneaky.rb │ ├── onboardings_controller.rb │ ├── sessions_controller.rb │ ├── slack_accounts_controller.rb │ ├── static_pages_controller.rb │ └── webhooks │ │ ├── aadhaar_controller.rb │ │ └── application_controller.rb ├── frontend │ ├── entrypoints │ │ ├── application.css │ │ ├── application.js │ │ ├── backend.css │ │ ├── backend.js │ │ └── direct_upload.js │ ├── images │ │ ├── .keep │ │ ├── hc-square.png │ │ ├── icons │ │ │ └── break-the-glass.png │ │ └── loader.gif │ ├── js │ │ ├── alpine.js │ │ ├── click-to-copy.js │ │ └── lightswitch.js │ └── stylesheets │ │ ├── application.scss │ │ ├── backend.scss │ │ ├── colors.css │ │ ├── layout.css │ │ ├── os9.css │ │ └── snippets │ │ ├── admin_tools.scss │ │ ├── banners.scss │ │ ├── borders.scss │ │ ├── brand.scss │ │ ├── footer.scss │ │ ├── forms.scss │ │ ├── lightswitch.scss │ │ └── tooltips.scss ├── helpers │ ├── addresses_helper.rb │ ├── api │ │ └── v1 │ │ │ ├── application_helper.rb │ │ │ └── identities_helper.rb │ ├── application_helper.rb │ ├── backend │ │ ├── application_helper.rb │ │ ├── audit_logs_helper.rb │ │ ├── identities_helper.rb │ │ ├── sessions_helper.rb │ │ └── users_helper.rb │ ├── credentials_helper.rb │ ├── onboarding_helper.rb │ └── static_pages_helper.rb ├── jobs │ ├── application_job.rb │ ├── identity │ │ └── notice_resemblances_job.rb │ ├── slack │ │ └── notify_guardians_job.rb │ └── verification │ │ ├── check_discrepancies_job.rb │ │ └── expire_draft_aadhaar_verifications_job.rb ├── mailers │ ├── application_mailer.rb │ ├── identity_mailer.rb │ └── verification_mailer.rb ├── models │ ├── address.rb │ ├── application_record.rb │ ├── backend.rb │ ├── backend │ │ ├── organizer_position.rb │ │ └── user.rb │ ├── break_glass_record.rb │ ├── concerns │ │ ├── .keep │ │ ├── country_enumable.rb │ │ └── public_identifiable.rb │ ├── identity.rb │ ├── identity │ │ ├── aadhaar_record.rb │ │ ├── document.rb │ │ ├── login_code.rb │ │ ├── resemblance.rb │ │ └── resemblance │ │ │ ├── email_subaddress_resemblance.rb │ │ │ ├── name_resemblance.rb │ │ │ └── reused_document_resemblance.rb │ ├── identity_program.rb │ ├── oauth_token.rb │ ├── program.rb │ ├── verification.rb │ └── verification │ │ ├── aadhaar_verification.rb │ │ ├── document_verification.rb │ │ └── vouch_verification.rb ├── policies │ ├── application_policy.rb │ ├── backend │ │ └── user_policy.rb │ ├── break_glass_record_policy.rb │ ├── identity │ │ └── document_policy.rb │ ├── identity_policy.rb │ ├── program_policy.rb │ ├── verification │ │ ├── aadhaar_verification_policy.rb │ │ ├── document_verification_policy.rb │ │ └── vouch_verification_policy.rb │ └── verification_policy.rb ├── services │ ├── aadhaar_service.rb │ ├── aadhaar_service │ │ ├── mock.rb │ │ └── production.rb │ ├── papers_please_engine.rb │ ├── papers_please_engine │ │ ├── aadhaar_scrutinizer.rb │ │ └── base.rb │ ├── resemblance_noticer_engine.rb │ ├── resemblance_noticer_engine │ │ ├── base.rb │ │ ├── duplicate_documents.rb │ │ ├── email_subaddressing.rb │ │ └── name_similarity.rb │ └── slack_service.rb └── views │ ├── aadhaar │ └── digilocker_link.html.erb │ ├── addresses │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ ├── program_create_address.html.erb │ └── show.html.erb │ ├── api │ └── v1 │ │ ├── addresses │ │ └── _address.jb │ │ └── identities │ │ ├── _identity.jb │ │ ├── index.jb │ │ ├── me.jb │ │ └── show.jb │ ├── backend │ ├── audit_logs │ │ └── index.html.erb │ ├── dashboard │ │ └── show.html.erb │ ├── identities │ │ ├── _identity.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new_vouch.html.erb │ │ └── show.html.erb │ ├── identity │ │ └── resemblance │ │ │ ├── email_subaddress_resemblances │ │ │ └── _email_subaddress_resemblance.html.erb │ │ │ ├── name_resemblances │ │ │ └── _name_resemblance.html.erb │ │ │ └── reused_document_resemblances │ │ │ └── _reused_document_resemblance.html.erb │ ├── programs │ │ ├── _program.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ ├── static_pages │ │ ├── index.html.erb │ │ ├── login.html.erb │ │ └── session_dump.html.erb │ ├── users │ │ ├── _user.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ └── verifications │ │ ├── index.html.erb │ │ ├── pending.html.erb │ │ └── show.html.erb │ ├── base.rb │ ├── doorkeeper │ ├── applications │ │ ├── _delete_form.html.erb │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ ├── authorizations │ │ ├── error.html.erb │ │ ├── form_post.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ └── authorized_applications │ │ ├── _delete_form.html.erb │ │ └── index.html.erb │ ├── forms │ ├── application_form.rb │ └── backend │ │ ├── programs │ │ └── form.rb │ │ └── users │ │ └── form.rb │ ├── kaminari │ ├── _first_page.html.erb │ ├── _gap.html.erb │ ├── _last_page.html.erb │ ├── _next_page.html.erb │ ├── _page.html.erb │ ├── _paginator.html.erb │ └── _prev_page.html.erb │ ├── layouts │ ├── application.html.erb │ ├── backend.html.erb │ ├── doorkeeper │ │ └── admin.html.erb │ └── mailer.text.erb │ ├── mailers │ └── blank_mailer.text.erb │ ├── onboardings │ ├── aadhaar.html.erb │ ├── aadhaar_step_2.html.erb │ ├── address.html.erb │ ├── basic_info.html.erb │ ├── document.html.erb │ ├── submitted.html.erb │ └── welcome.html.erb │ ├── public_activity │ ├── break_glass_record │ │ └── _create.html.erb │ ├── identity │ │ ├── _admin_update.html.erb │ │ ├── _clear_slack_id.html.erb │ │ ├── _create.html.erb │ │ ├── _set_slack_id.html.erb │ │ └── _update.html.erb │ ├── verification │ │ ├── _approve.html.erb │ │ ├── _create.html.erb │ │ └── _reject.html.erb │ ├── verification_aadhaar_verification │ │ ├── _create.html.erb │ │ ├── _create_link.html.erb │ │ └── _data_received.html.erb │ ├── verification_document_verification │ │ ├── _approve.html.erb │ │ ├── _create.html.erb │ │ ├── _ignored.html.erb │ │ ├── _reject.html.erb │ │ └── _update.html.erb │ └── verification_vouch_verification │ │ ├── _create.html.erb │ │ └── _ignored.html.erb │ ├── pwa │ ├── manifest.json.erb │ └── service-worker.js │ ├── sessions │ ├── check_your_email.html.erb │ ├── new.html.erb │ └── verify.html.erb │ ├── shared │ ├── _flash.html.erb │ ├── _verification_status.html.erb │ └── async_flash.erb │ └── static_pages │ ├── external_api_docs.html.erb │ ├── faq.html.erb │ └── index.html.erb ├── bin ├── brakeman ├── bundle ├── dev ├── docker-entrypoint ├── lint ├── rails ├── rake ├── rubocop ├── setup ├── thrust └── vite ├── config.ru ├── config ├── application.rb ├── boot.rb ├── brakeman.ignore ├── credentials.yml.enc ├── credentials │ ├── production.yml.enc │ └── staging.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ ├── staging.rb │ └── test.rb ├── honeybadger.yml ├── initializers │ ├── content_security_policy.rb │ ├── doorkeeper.rb │ ├── filter_parameter_logging.rb │ ├── flipper.rb │ ├── git_version.rb │ ├── good_job.rb │ ├── inflections.rb │ ├── monkey_patches.rb │ ├── paper_trail.rb │ ├── phlex.rb │ ├── public_activity.rb │ └── slack.rb ├── locales │ ├── doorkeeper.en.yml │ └── en.yml ├── puma.rb ├── routes.rb ├── sanctioned_countries.yml ├── storage.yml └── vite.json ├── db ├── migrate │ ├── 20250822205652_init_schema.rb │ └── 20250902193412_add_extra_data_to_versions.rb ├── schema.rb └── seeds.rb ├── docker-compose-dbonly.yml ├── lib ├── application_component.rb └── tasks │ └── .keep ├── log └── .keep ├── package.json ├── public ├── .well-known │ └── security.txt ├── 400.html ├── 404.html ├── 406-unsupported-browser.html ├── 422.html ├── 500.html ├── ChicagoFLF.ttf ├── icon.png ├── icon.svg └── robots.txt ├── script └── .keep ├── storage └── .keep ├── tmp ├── .keep └── storage │ └── .keep ├── vendor └── .keep ├── vite.config.mts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. 2 | 3 | # Ignore git directory. 4 | /.git/ 5 | /.gitignore 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all default key files. 14 | /config/master.key 15 | /config/credentials/*.key 16 | 17 | # Ignore all logfiles and tempfiles. 18 | /log/* 19 | /tmp/* 20 | !/log/.keep 21 | !/tmp/.keep 22 | 23 | # Ignore pidfiles, but keep the directory. 24 | /tmp/pids/* 25 | !/tmp/pids/.keep 26 | 27 | # Ignore storage (uploaded files in development and any SQLite databases). 28 | /storage/* 29 | !/storage/.keep 30 | /tmp/storage/* 31 | !/tmp/storage/.keep 32 | 33 | # Ignore assets. 34 | /node_modules/ 35 | /app/assets/builds/* 36 | !/app/assets/builds/.keep 37 | /public/assets 38 | 39 | # Ignore CI service files. 40 | /.github 41 | 42 | # Ignore development files 43 | /.devcontainer 44 | 45 | # Ignore Docker-related files 46 | /.dockerignore 47 | /Dockerfile* 48 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | config/credentials/*.yml.enc diff=rails_credentials 9 | config/credentials.yml.enc diff=rails_credentials 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ main ] 7 | 8 | jobs: 9 | scan_ruby: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: .ruby-version 20 | bundler-cache: true 21 | 22 | - name: Scan for common Rails security vulnerabilities using static analysis 23 | run: bin/brakeman --no-pager 24 | 25 | lint: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Set up Ruby 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: .ruby-version 35 | bundler-cache: true 36 | 37 | - name: Lint code for consistent style 38 | run: bin/rubocop -f github 39 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # Temporary files generated by your text editor or operating system 4 | # belong in git's global ignore instead: 5 | # `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log/* 15 | /tmp/* 16 | !/log/.keep 17 | !/tmp/.keep 18 | 19 | # Ignore pidfiles, but keep the directory. 20 | /tmp/pids/* 21 | !/tmp/pids/ 22 | !/tmp/pids/.keep 23 | 24 | # Ignore storage (uploaded files in development and any SQLite databases). 25 | /storage/* 26 | !/storage/.keep 27 | /tmp/storage/* 28 | !/tmp/storage/ 29 | !/tmp/storage/.keep 30 | 31 | /public/assets 32 | 33 | # Ignore master key for decrypting credentials and more. 34 | /config/master.key 35 | 36 | # Vite Ruby 37 | /public/vite* 38 | node_modules 39 | # Vite uses dotenv and suggests to ignore local-only env files. See 40 | # https://vitejs.dev/guide/env-and-mode.html#env-files 41 | *.local 42 | 43 | 44 | /config/credentials/production.key 45 | 46 | /config/credentials/staging.key 47 | 48 | /config/credentials/development.key 49 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Omakase Ruby styling for Rails 2 | inherit_gem: { rubocop-rails-omakase: rubocop.yml } 3 | 4 | # Overwrite or add rules to create your own house style 5 | # 6 | # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` 7 | # Layout/SpaceInsideArrayLiteralBrackets: 8 | # Enabled: false 9 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.4 2 | -------------------------------------------------------------------------------- /Dockerfile.worker: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # check=error=true 3 | 4 | # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: 5 | # docker build -t identity_vault . 6 | # docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name identity_vault identity_vault 7 | 8 | # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html 9 | 10 | # Make sure RUBY_VERSION matches the Ruby version in .ruby-version 11 | ARG RUBY_VERSION=3.4.4 12 | FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base 13 | 14 | # Rails app lives here 15 | WORKDIR /rails 16 | 17 | # Install base packages 18 | RUN apt-get update -qq && \ 19 | apt-get install --no-install-recommends -y curl libjemalloc2 libvips imagemagick libheif-dev postgresql-client libffi-dev && \ 20 | apt-get clean && \ 21 | rm -rf /var/lib/apt/lists/* 22 | 23 | ENV BUNDLE_DEPLOYMENT="1" \ 24 | BUNDLE_PATH="/usr/local/bundle" \ 25 | BUNDLE_WITHOUT="development" 26 | 27 | # Throw-away build stage to reduce size of final image 28 | FROM base AS build 29 | 30 | # Install packages needed to build gems 31 | RUN apt-get update -qq && \ 32 | 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 && \ 33 | apt-get clean && \ 34 | rm -rf /var/lib/apt/lists/* 35 | 36 | # Install application gems 37 | COPY Gemfile Gemfile.lock ./ 38 | RUN bundle install && \ 39 | rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ 40 | bundle exec bootsnap precompile --gemfile 41 | 42 | # Copy application code 43 | COPY . . 44 | 45 | # Precompile bootsnap code for faster boot times 46 | RUN bundle exec bootsnap precompile app/ lib/ 47 | 48 | # Final stage for app image 49 | FROM base 50 | 51 | # Copy built artifacts: gems, application 52 | COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" 53 | COPY --from=build /rails /rails 54 | 55 | # Run and own only the runtime files as a non-root users for security 56 | RUN groupadd --system --gid 1000 rails && \ 57 | useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ 58 | chown -R rails:rails db log storage tmp 59 | USER 1000:1000 60 | 61 | CMD ["bundle", "exec", "good_job", "start"] -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | 2 | vite: bin/vite dev 3 | web: bin/rails s 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Identity Vault 2 | 3 | This is the Rails codebase powering https://identity.hackclub.com! 4 | 5 | ## contributing 6 | 7 | ask around in [#idv-dev](https://hackclub.slack.com/archives/C09D1E22CF5) or poke [nora](https://hackclub.slack.com/team/U06QK6AG3RD)! 8 | 9 | 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 10 | 11 | kindly `bin/lint` your code before you submit it! 12 | 13 | ### areas of focus 14 | 15 | the ops view components (look in `app/components`) are a hot mess... 16 | 17 | so is the onboarding controller, she should really be ripped out and replaced. 18 | 19 | ## dev setup 20 | 21 | - make sure you have working installations of ruby ≥ 3.4.4 & nodejs 22 | - clone repo 23 | - create .env.development, populate `DATABASE_URL` w/ a local postgres instance 24 | - if you want to use docker, you can run `docker compose -f docker-compose-dbonly.yml up` to spin up a database and plug `postgresql://postgres@localhost:5432/identity_vault_development` in as your `DATABASE_URL` 25 | - run `bundle install` 26 | - run `rails db:prepare` 27 | - console in (`bin/rails console`) 28 | - `Backend::User.create!(slack_id: "U", username: "", active: true, super_admin: true)` 29 | - run `bin/dev` (and `bin/vite dev` if you want hot reload on css & js) 30 | - visit `http://localhost:3000/backend/login`, paste that Slack ID in, and "fake it til' you make it" 31 | 32 | ## security 33 | 34 | 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 – 35 | 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. 36 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* Application styles */ 2 | -------------------------------------------------------------------------------- /app/components/api_example.rb: -------------------------------------------------------------------------------- 1 | class Components::APIExample < Components::Base 2 | extend Literal::Properties 3 | prop :method, _Nilable(String) 4 | prop :url, _Nilable(String) 5 | prop :path_only, _Boolean? 6 | 7 | def view_template 8 | div style: { margin: "10px 0" } do 9 | code style: { background: "black", padding: "0.2em", color: "white" } do 10 | span style: { color: "cyan" } do 11 | @method 12 | end 13 | plain " " 14 | copy_to_clipboard @url do 15 | if @path_only 16 | CGI.unescape(URI.parse(@url).tap { |u| u.host = u.scheme = u.port = nil }.to_s) 17 | else 18 | @url 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/components/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Components::Base < Phlex::HTML 4 | include Components 5 | 6 | # Include any helpers you want to be available across all components 7 | include Phlex::Rails::Helpers::Routes 8 | include Phlex::Rails::Helpers::FormWith 9 | include Phlex::Rails::Helpers::DistanceOfTimeInWords 10 | 11 | # Register Rails form helpers 12 | register_value_helper :form_authenticity_token 13 | register_value_helper :dev_tool 14 | register_output_helper :vite_image_tag 15 | register_value_helper :ap 16 | register_output_helper :copy_to_clipboard 17 | 18 | if Rails.env.development? 19 | def before_template 20 | comment { "Before #{self.class.name}" } 21 | super 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/components/bootleg_turbo.rb: -------------------------------------------------------------------------------- 1 | class Components::BootlegTurbo < Components::Base 2 | def initialize(path, text: nil, **opts) 3 | @path = path 4 | @text = text 5 | @opts = opts 6 | end 7 | 8 | def view_template 9 | div(hx_get: @path, hx_trigger: :load, **@opts) do 10 | if @text 11 | plain @text 12 | br 13 | end 14 | vite_image_tag "images/loader.gif", class: :htmx_indicator, style: "image-rendering: pixelated;" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/components/brand.rb: -------------------------------------------------------------------------------- 1 | class Components::Brand < Components::Base 2 | def initialize(identity:) 3 | @identity = identity 4 | end 5 | 6 | def view_template 7 | div(class: "brand") do 8 | if @identity.present? 9 | copy_to_clipboard @identity.public_id, tooltip_direction: "e", label: "click to copy your internal ID" do 10 | logo 11 | end 12 | else 13 | logo 14 | end 15 | h1 { "Hack Club Identity" } 16 | end 17 | button id: "lightswitch", class: "lightswitch-btn", type: "button", "aria-label": "Toggle theme" do 18 | span class: "lightswitch-icon" do 19 | "🌙" 20 | end 21 | end 22 | case Rails.env 23 | when "staging" 24 | div(class: "banner purple") do 25 | safe "this is a staging environment. do not upload any actual personal information here." 26 | end 27 | when "development" 28 | div(class: "banner success") do 29 | plain "you're in dev! go nuts :3" 30 | end 31 | end 32 | end 33 | 34 | def logo 35 | vite_image_tag "images/hc-square.png", alt: "Hack Club logo", class: "brand-logo" 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/components/break_the_glass.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Components::BreakTheGlass < Components::Base 4 | attr_reader :break_glassable, :auto_break_glass 5 | 6 | def initialize(break_glassable, auto_break_glass: nil) 7 | @break_glassable = break_glassable 8 | @auto_break_glass = auto_break_glass 9 | end 10 | 11 | def view_template 12 | if glass_broken? 13 | yield if block_given? 14 | else 15 | render_break_the_glass 16 | end 17 | end 18 | 19 | private 20 | 21 | def glass_broken? 22 | return false unless helpers.user_signed_in? 23 | 24 | # Check if a recent break glass record already exists 25 | existing_record = BreakGlassRecord.for_user_and_document(helpers.current_user, @break_glassable) 26 | .recent 27 | .exists? 28 | 29 | return true if existing_record 30 | 31 | # If auto_break_glass is enabled, automatically create a break glass record 32 | if @auto_break_glass 33 | BreakGlassRecord.create!( 34 | backend_user: helpers.current_user, 35 | break_glassable: @break_glassable, 36 | reason: @auto_break_glass, 37 | accessed_at: Time.current, 38 | automatic: true, 39 | ) 40 | return true 41 | end 42 | 43 | false 44 | end 45 | 46 | def render_break_the_glass 47 | div do 48 | render Components::BreakTheGlassForm.new(@break_glassable) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/components/break_the_glass_form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Components::BreakTheGlassForm < Components::Base 4 | def initialize(break_glassable) 5 | @break_glassable = break_glassable 6 | end 7 | 8 | def view_template 9 | div(class: "break-glass-form", style: "padding: 2rem;") do 10 | div(style: "margin-bottom: 1rem;") do 11 | vite_image_tag "images/icons/break-the-glass.png", style: "width: 64px; image-rendering: pixelated;" 12 | div(style: "display: inline-block; vertical-align: top; margin-left: 0.5em;") do 13 | h1(style: "margin 0; display: inline-block; vertical-align: top;") { "Break the Glass" } 14 | br 15 | plain "This #{document_type} has already been reviewed." 16 | br 17 | plain "Please affirm that you have a legitimate need to view this #{document_type}." 18 | end 19 | end 20 | 21 | form_with url: "/backend/break_glass", method: :post, local: true, style: "max-width: 400px; margin: 0 auto;" do |form| 22 | form.hidden_field :break_glassable_id, value: @break_glassable.id 23 | form.hidden_field :break_glassable_type, value: @break_glassable.class.name 24 | 25 | div(style: "display: flex; align-items: center; gap: 0.5em;") do 26 | p { "I'm accessing this #{document_type} " } 27 | form.text_field :reason, placeholder: "because i'm investigating a fraud claim", style: "width: 30%;" 28 | form.submit "i promise.", class: "button button-primary" 29 | end 30 | end 31 | end 32 | end 33 | 34 | private 35 | 36 | def document_type 37 | case @break_glassable.class.name 38 | when "Identity::Document" 39 | "identity document" 40 | when "Identity::AadhaarRecord" 41 | "aadhaar record" 42 | else 43 | "document" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/components/footer.rb: -------------------------------------------------------------------------------- 1 | class Components::Footer < Components::Base 2 | include Phlex::Rails::Helpers::TimeAgoInWords 3 | 4 | def view_template 5 | footer(class: "app-footer") do 6 | div(class: "footer-content") do 7 | div(class: "footer-main") do 8 | p(class: "app-name") { "Identity Vault" } 9 | end 10 | 11 | div(class: "footer-version") do 12 | div(class: "version-info") do 13 | p do 14 | plain "Build " 15 | if git_version.present? 16 | if commit_link.present? 17 | a(href: commit_link, target: "_blank", class: "version-link") do 18 | "v#{git_version}" 19 | end 20 | else 21 | span(class: "version-text") { "v#{git_version}" } 22 | end 23 | end 24 | plain " from #{time_ago_in_words(server_start_time)} ago" 25 | end 26 | end 27 | end 28 | 29 | div(class: "environment-badge #{Rails.env.downcase}") do 30 | Rails.env.upcase 31 | end 32 | end 33 | end 34 | end 35 | 36 | def git_version = Rails.application.config.try(:git_version) 37 | 38 | def commit_link = Rails.application.config.try(:commit_link) 39 | 40 | def server_start_time 41 | Rails.application.config.try(:server_start_time) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/components/home_button.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Components::HomeButton < Components::Base 4 | def view_template 5 | a href: root_path do 6 | "← back to home" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/components/identity.rb: -------------------------------------------------------------------------------- 1 | class Components::Identity < Components::Base 2 | attr_reader :identity 3 | 4 | def initialize(identity, show_legal_name: false) 5 | @identity = identity 6 | @show_legal_name = show_legal_name 7 | end 8 | 9 | def field(label, value = nil) 10 | b { "#{label}: " } 11 | if block_given? 12 | yield 13 | else 14 | span { value.to_s } 15 | end 16 | br 17 | end 18 | 19 | def view_template 20 | div do 21 | render @identity 22 | br 23 | if @identity.legal_first_name.present? && @show_legal_name 24 | field "Legal First Name", @identity.legal_first_name 25 | field "Legal Last Name", @identity.legal_last_name 26 | end 27 | field "Country", @identity.country 28 | field "Primary Email", @identity.primary_email 29 | field "Birthday", @identity.birthday 30 | field "Phone", @identity.phone_number 31 | field "Verification status", @identity.verification_status.humanize 32 | if defined?(@identity.ysws_eligible) && !@identity.ysws_eligible.nil? 33 | field "YSWS eligible", @identity.ysws_eligible 34 | end 35 | 36 | field "Slack ID" do 37 | if identity.slack_id.present? 38 | a(href: "https://hackclub.slack.com/team/#{identity.slack_id}") do 39 | identity.slack_id 40 | end 41 | copy_to_clipboard(identity.slack_id) do 42 | plain " (copy)" 43 | end 44 | else 45 | plain "not set" 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/components/identity_review/aadhaar_full.rb: -------------------------------------------------------------------------------- 1 | class Components::IdentityReview::AadhaarFull < Components::Base 2 | def initialize(aadhaar_record) 3 | @aadhaar_record = aadhaar_record 4 | @data = @aadhaar_record.doc_json[:data] 5 | end 6 | 7 | def field(key, name) 8 | res = @data.dig(key) 9 | return if res.blank? 10 | li do 11 | b { "#{name}: " } 12 | plain res 13 | end 14 | end 15 | 16 | def view_template 17 | h2 { "Full Aadhaar data:" } 18 | br 19 | 20 | ul style: { list_style_type: "disc" } do 21 | li do 22 | b { "Photo:" } 23 | br 24 | img src: "data:image/jpeg;base64,#{@data[:photo]}", style: { width: "100px", margin_left: "1em" } 25 | end 26 | field :name, "Full name" 27 | field :"Father Name", "Father's name" 28 | field :dob, "Date of birth" 29 | field :aadhar_number, "Aadhaar number" 30 | field :gender, "Assigned gender" 31 | field :co, "C/O" 32 | li do 33 | b { "Address:" } 34 | ul style: { margin_left: "1rem", list_style_type: "square" } do 35 | @data.dig(:address).each do |key, value| 36 | next if value.blank? 37 | li { b { "#{key}: " }; plain value } 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/components/identity_review/aadhaar_info.rb: -------------------------------------------------------------------------------- 1 | class Components::IdentityReview::AadhaarInfo < Components::Base 2 | def initialize(verification) 3 | @verification = verification 4 | end 5 | 6 | def view_template 7 | div class: "lowered padding" do 8 | h2(style: "margin-top: 0;") { "Aadhaar Information" } 9 | table style: "width: 100%;" do 10 | tr do 11 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Aadhaar Number:" } 12 | td(style: "padding: 0.25rem 0;") { @verification.identity.aadhaar_number } 13 | end 14 | tr do 15 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Uploaded:" } 16 | td(style: "padding: 0.25rem 0;") { @verification.pending_at&.strftime("%B %d, %Y at %I:%M %p") || "N/A" } 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/components/identity_review/basic_details.rb: -------------------------------------------------------------------------------- 1 | class Components::IdentityReview::BasicDetails < Components::Base 2 | def initialize(identity) 3 | @identity = identity 4 | end 5 | 6 | def view_template 7 | div class: "lowered padding" do 8 | h2(style: "margin-top: 0;") { "Identity Information" } 9 | table style: "width: 100%;" do 10 | tr do 11 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Name:" } 12 | td(style: "padding: 0.25rem 0;") { render(@identity) } 13 | end 14 | if @identity.legal_first_name.present? 15 | tr do 16 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Legal Name:" } 17 | td(style: "padding: 0.25rem 0;") { 18 | "#{@identity.legal_first_name} #{@identity.legal_last_name}" 19 | } 20 | end 21 | end 22 | tr do 23 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Email:" } 24 | td(style: "padding: 0.25rem 0;") { @identity.primary_email } 25 | end 26 | tr do 27 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Birthday:" } 28 | td(style: "padding: 0.25rem 0;") { @identity.birthday.strftime("%B %d, %Y") } 29 | end 30 | tr do 31 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Age:" } 32 | td(style: "padding: 0.25rem 0;") { @identity.age.round(2) } 33 | end 34 | tr do 35 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Country:" } 36 | td(style: "padding: 0.25rem 0;") { @identity.country } 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/components/identity_review/document_info.rb: -------------------------------------------------------------------------------- 1 | class Components::IdentityReview::DocumentInfo < Components::Base 2 | def initialize(verification) 3 | @verification = verification 4 | end 5 | 6 | def view_template 7 | div class: "lowered padding" do 8 | h2(style: "margin-top: 0;") { "Document Information" } 9 | table style: "width: 100%;" do 10 | tr do 11 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Type:" } 12 | td(style: "padding: 0.25rem 0;") { @verification.document_type } 13 | end 14 | tr do 15 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Uploaded:" } 16 | td(style: "padding: 0.25rem 0;") { @verification.identity_document.created_at.strftime("%B %d, %Y at %I:%M %p") } 17 | end 18 | if @verification.identity.country == "IN" 19 | tr do 20 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Suggested Aadhaar password:" } 21 | td(style: "padding: 0.25rem 0;") { copy_to_clipboard(@verification.identity.suggested_aadhaar_password, tooltip_direction: "e") } 22 | end 23 | end 24 | tr do 25 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Status:" } 26 | td(style: "padding: 0.25rem 0;") do 27 | span class: (@verification.pending? ? "status-pending" : @verification.approved? ? "status-verified" : "status-rejected") do 28 | @verification.status.humanize 29 | end 30 | end 31 | end 32 | tr do 33 | td(style: "font-weight: bold; padding: 0.25rem 0;") { "Files:" } 34 | td(style: "padding: 0.25rem 0;") { @verification.identity_document.files.count } 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/components/inspector.rb: -------------------------------------------------------------------------------- 1 | class Components::Inspector < Components::Base 2 | def initialize(record, small: false) 3 | @record = record 4 | @small = small 5 | @id_line = "#{@record.class.name}#{" record" unless @small} #{@record&.try(:public_id) || @record&.id}" 6 | end 7 | 8 | def view_template 9 | return unless Rails.env.development? 10 | 11 | details(class: @small ? nil : "dev-tool") do 12 | summary { "#{"Inspect" unless @small} #{@id_line}" } 13 | pre class: %i[input readonly] do 14 | unless @record.nil? 15 | raw safe(ap @record) 16 | else 17 | "no record?" 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/components/public_activity/container.rb: -------------------------------------------------------------------------------- 1 | class Components::PublicActivity::Container < Components::Base 2 | register_value_helper :render_activities 3 | 4 | def initialize(activities) 5 | @activities = activities 6 | end 7 | 8 | def view_template 9 | table class: %i[table detailed] do 10 | thead do 11 | tr do 12 | th { "User" } 13 | th { "Action" } 14 | th { "Time" } 15 | th { "Inspect" } if Rails.env.development? 16 | end 17 | end 18 | tbody do 19 | render_activities(@activities) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/components/public_activity/snippet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Components::PublicActivity::Snippet < Components::Base 4 | def initialize(activity, owner: nil) 5 | @activity = activity 6 | @owner = owner 7 | end 8 | 9 | def view_template 10 | tr do 11 | td { render @owner || @activity.owner } 12 | td { yield } 13 | td { @activity.created_at.strftime("%Y-%m-%d %H:%M:%S") } 14 | td do 15 | if Rails.env.development? 16 | render Components::Inspector.new(@activity, small: true) 17 | render Components::Inspector.new(@activity.trackable, small: true) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/components/resemblance.rb: -------------------------------------------------------------------------------- 1 | class Components::Resemblance < Components::Base 2 | attr_reader :resemblance 3 | 4 | def initialize(resemblance) 5 | @resemblance = resemblance 6 | end 7 | 8 | def view_template 9 | div style: { border: "1px solid", padding: "10px", margin: "10px" } do 10 | render @resemblance 11 | render Components::Identity.new(@resemblance.past_identity) 12 | render Components::Inspector.new(@resemblance) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/components/user_mention.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Components::UserMention < Components::Base 4 | extend Literal::Properties 5 | 6 | prop :user, _Union(Backend::User, ::Identity), :positional 7 | 8 | def view_template 9 | div class: "icon", role: "option" do 10 | case @user 11 | when Backend::User 12 | img src: @user.icon_url, width: "16px", class: "inline pr-2" 13 | div class: "icon-label" do 14 | a(href: backend_user_path(@user)) do 15 | span { @user.username } 16 | span { " ⚡" } if @user.super_admin? 17 | end 18 | end 19 | when ::Identity 20 | div(class: "inline pr-2") { "🪪" } 21 | div class: "icon-label" do 22 | a(href: backend_identity_path(@user)) { span { "#{@user.first_name} #{@user.last_name}" } } 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/components/window.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Components::Window < Components::Base 4 | extend Literal::Properties 5 | 6 | prop :window_title, String, :positional 7 | prop :close_url, _Nilable(String) 8 | prop :maximize_url, _Nilable(String) 9 | prop :max_width, Integer, default: 400.freeze 10 | 11 | def view_template 12 | div class: "window active", style: "max-width: #{@max_width}px" do 13 | div class: "title-bar" do 14 | div(class: "title-bar-text") { @window_title } 15 | if @close_url || @maximize_url 16 | div class: "title-bar-buttons" do 17 | button(data_maximize: "", onclick: safe("window.location.href='#{@maximize_url}'")) if @maximize_url 18 | button(data_close: "", onclick: safe("window.location.href='#{@close_url}'")) if @close_url 19 | end 20 | end 21 | end 22 | div class: "window-body" do 23 | yield 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/aadhaar_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AadhaarController < ApplicationController 4 | before_action :ensure_step, :set_verification 5 | layout false 6 | 7 | def async_digilocker_link 8 | begin 9 | @verification.generate_link!( 10 | callback_url: webhooks_aadhaar_callback_url( 11 | Rails.application.credentials.dig(:aadhaar, :webhook_secret) 12 | ), 13 | redirect_url: submitted_onboarding_url, 14 | ) unless @verification.aadhaar_external_transaction_id.present? 15 | 16 | render :digilocker_link 17 | rescue StandardError => e 18 | uuid = Honeybadger.notify(e) 19 | response.set_header("HX-Retarget", "#async_flash") 20 | render "shared/async_flash", locals: { f: { error: "error generating digilocker link – #{e.message} #{uuid}" } } 21 | end 22 | end 23 | 24 | def digilocker_redirect 25 | redirect_to @verification.aadhaar_link, allow_other_host: true 26 | end 27 | 28 | private 29 | 30 | def set_verification 31 | @verification = current_identity.aadhaar_verifications.draft.first 32 | end 33 | 34 | def ensure_step 35 | render html: "🥐" unless current_identity&.onboarding_step == :aadhaar 36 | 37 | if current_identity&.verification_status == "ineligible" 38 | redirect_to submitted_onboarding_path and return 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/api/external/application_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | module External 3 | class ApplicationController < ActionController::API 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/api/external/identities_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | module External 3 | class IdentitiesController < ApplicationController 4 | def check 5 | ident = if (public_id = params[:idv_id]).present? 6 | Identity.find_by_public_id(public_id) 7 | elsif (primary_email = params[:email]).present? 8 | Identity.find_by(primary_email:) 9 | elsif (slack_id = params[:slack_id]).present? 10 | Identity.find_by(slack_id:) 11 | else 12 | raise ActionController::ParameterMissing, "provide one of: idv_id, email, slack_id" 13 | end 14 | 15 | result = if ident 16 | case ident.verification_status 17 | when "needs_submission", "pending" 18 | ident.verification_status 19 | when "verified" 20 | ident.ysws_eligible? ? "verified_eligible" : "verified_but_over_18" 21 | when "ineligible" 22 | "rejected" 23 | else 24 | "unknown" 25 | end 26 | else 27 | "not_found" 28 | end 29 | render json: { 30 | result: 31 | } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/api/v1/application_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | module V1 3 | class ApplicationController < ActionController::API 4 | prepend_view_path "app/views/api/v1" 5 | 6 | helper_method :current_identity, :current_program, :current_scopes, :acting_as_program 7 | 8 | attr_reader :current_identity 9 | attr_reader :current_program 10 | attr_reader :current_scopes 11 | attr_reader :acting_as_program 12 | 13 | before_action :authenticate! 14 | 15 | include ActionController::HttpAuthentication::Token::ControllerMethods 16 | 17 | rescue_from Pundit::NotAuthorizedError do |e| 18 | render json: { error: "not_authorized" }, status: :forbidden 19 | end 20 | 21 | rescue_from ActionController::ParameterMissing do |e| 22 | render json: { error: e.message }, status: :bad_request 23 | end 24 | 25 | private 26 | 27 | def authenticate! 28 | @current_token = authenticate_with_http_token do |t, _options| 29 | OAuthToken.find_by(token: t) || Program.find_by(program_key: t) 30 | end 31 | unless @current_token&.active? 32 | return render json: { error: "invalid_auth" }, status: :unauthorized 33 | end 34 | if @current_token.is_a?(OAuthToken) 35 | @current_identity = @current_token.resource_owner 36 | @current_program = @current_token.application 37 | unless @current_program&.active? 38 | return render json: { error: "invalid_auth" }, status: :unauthorized 39 | end 40 | else 41 | @acting_as_program = true 42 | @current_program = @current_token 43 | end 44 | @current_scopes = @current_program.scopes 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/controllers/api/v1/hcb_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | module V1 3 | class HCBController < ApplicationController 4 | skip_before_action :authenticate! 5 | 6 | def show 7 | render json: { pending: Verification.where(status: "pending").count } 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/api/v1/health_check_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | module V1 3 | class HealthCheckController < ApplicationController 4 | skip_before_action :authenticate! 5 | def show 6 | _ = Identity.last 7 | render json: { message: "we're chillin'" } 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/api/v1/identities_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | module V1 3 | class IdentitiesController < ApplicationController 4 | def me 5 | @identity = current_identity 6 | raise ActiveRecord::RecordNotFound unless current_identity 7 | render :me 8 | end 9 | 10 | def show 11 | raise Pundit::NotAuthorizedError unless acting_as_program 12 | @identity = ident_scope.find_by_public_id!(params[:id]) 13 | render :show 14 | end 15 | 16 | def set_slack_id 17 | raise Pundit::NotAuthorizedError unless acting_as_program && current_scopes.include?("set_slack_id") 18 | @identity = ident_scope.find_by_public_id!(params[:id]) 19 | 20 | if @identity.slack_id.present? 21 | return render json: { message: "slack already associated?" } 22 | end 23 | 24 | @identity.update!(slack_id: params.require(:slack_id)) 25 | @identity.create_activity(key: "identity.set_slack_id", owner: current_program) 26 | render :show 27 | end 28 | 29 | def index 30 | raise Pundit::NotAuthorizedError unless acting_as_program 31 | @identities = ident_scope.all 32 | render :index 33 | end 34 | 35 | private 36 | 37 | def ident_scope 38 | current_program.identities 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include PublicActivity::StoreController 3 | include IsSneaky 4 | 5 | helper_method :current_identity, :identity_signed_in?, :current_onboarding_step 6 | 7 | before_action :invalidate_v1_sessions, :authenticate_identity!, :set_honeybadger_context 8 | 9 | before_action :set_paper_trail_whodunnit 10 | 11 | def current_identity 12 | @current_identity ||= Identity.find_by(id: session[:identity_id]) if session[:identity_id] 13 | end 14 | 15 | alias_method :user_for_public_activity, :current_identity 16 | 17 | def user_for_paper_trail = current_identity&.id 18 | 19 | def identity_signed_in? = !!current_identity 20 | 21 | 22 | def invalidate_v1_sessions 23 | if cookies["_identity_vault_session"] 24 | cookies.delete("_identity_vault_session", 25 | path: "/", 26 | secure: Rails.env.production?, 27 | httponly: true) 28 | end 29 | end 30 | 31 | def authenticate_identity! 32 | unless identity_signed_in? 33 | session[:oauth_return_to] = request.original_url unless request.xhr? 34 | # JANK ALERT 35 | hide_some_data_away 36 | 37 | # EW 38 | return if controller_name == "onboardings" 39 | 40 | redirect_to welcome_onboarding_path 41 | end 42 | end 43 | 44 | def set_honeybadger_context 45 | Honeybadger.context({ 46 | identity_id: current_identity&.id 47 | }) 48 | end 49 | 50 | def current_onboarding_step 51 | identity = current_identity 52 | 53 | return :basic_info unless identity&.persisted? 54 | return :document unless identity.verifications.where(status: [ "approved", "pending" ]).any? 55 | 56 | :submitted 57 | rescue => e 58 | Rails.logger.error "Error determining onboarding step: #{e.message}" 59 | :basic_info 60 | end 61 | 62 | rescue_from ActiveRecord::RecordNotFound do |e| 63 | flash[:error] = "sorry, couldn't find that object... (404)" 64 | redirect_to root_path 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /app/controllers/backend/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Backend 2 | class ApplicationController < ActionController::Base 3 | include PublicActivity::StoreController 4 | include Pundit::Authorization 5 | 6 | layout "backend" 7 | 8 | after_action :verify_authorized 9 | 10 | helper_method :current_user, :user_signed_in? 11 | 12 | before_action :authenticate_user!, :set_honeybadger_context 13 | 14 | before_action :set_paper_trail_whodunnit 15 | 16 | def current_user 17 | @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] 18 | end 19 | 20 | def current_impersonator 21 | @current_impersonator ||= User.find_by(id: session[:impersonator_user_id]) if session[:impersonator_user_id] 22 | end 23 | 24 | alias_method :find_current_auditor, :current_user 25 | alias_method :user_for_public_activity, :current_user 26 | 27 | def user_for_paper_trail = current_impersonator&.id || current_user&.id 28 | 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 } 29 | 30 | def user_signed_in? = !!current_user 31 | 32 | def authenticate_user! 33 | unless user_signed_in? 34 | return redirect_to backend_login_path, alert: ("you need to be logged in!") 35 | end 36 | unless @current_user&.active? 37 | session[:user_id] = nil 38 | @current_user = nil 39 | redirect_to backend_login_path, alert: ("you need to be logged in!") 40 | end 41 | end 42 | 43 | def set_honeybadger_context 44 | Honeybadger.context({ 45 | user_id: current_user&.id, 46 | user_username: current_user&.username 47 | }) 48 | end 49 | 50 | rescue_from Pundit::NotAuthorizedError do |e| 51 | flash[:error] = "you don't seem to be authorized to do that?" 52 | redirect_to backend_root_path 53 | end 54 | 55 | rescue_from ActiveRecord::RecordNotFound do |e| 56 | flash[:error] = "sorry, couldn't find that object... (404)" 57 | redirect_to backend_root_path 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/controllers/backend/audit_logs_controller.rb: -------------------------------------------------------------------------------- 1 | module Backend 2 | class AuditLogsController < ApplicationController 3 | skip_after_action :verify_authorized 4 | 5 | def index 6 | scope = PublicActivity::Activity.order(created_at: :desc) 7 | scope = scope.where(owner_type: "Backend::User") if params[:admin_actions_only] 8 | 9 | @activities = scope.page(params[:page]).per(50) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/backend/break_glass_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Backend::BreakGlassController < Backend::ApplicationController 4 | def create 5 | @break_glassable = find_break_glassable 6 | 7 | authorize BreakGlassRecord 8 | 9 | break_glass_record = BreakGlassRecord.new( 10 | backend_user: current_user, 11 | break_glassable: @break_glassable, 12 | reason: params[:reason], 13 | accessed_at: Time.current, 14 | ) 15 | 16 | if break_glass_record.save 17 | redirect_back(fallback_location: backend_root_path, notice: "Access granted. #{document_type.capitalize} is now visible.") 18 | else 19 | redirect_back(fallback_location: backend_root_path, alert: "Failed to grant access: #{break_glass_record.errors.full_messages.join(", ")}") 20 | end 21 | end 22 | 23 | private 24 | 25 | # it'd be neat if this was polymorphic 26 | def find_break_glassable 27 | case params[:break_glassable_type] 28 | when "Identity::Document" 29 | Identity::Document.find(params[:break_glassable_id]) 30 | when "Identity::AadhaarRecord" 31 | Identity::AadhaarRecord.find(params[:break_glassable_id]) 32 | when "Identity" 33 | Identity.find_by_public_id!(params[:break_glassable_id]) 34 | else 35 | raise ArgumentError, "Invalid break_glassable_type: #{params[:break_glassable_type]}" 36 | end 37 | end 38 | 39 | # TODO: these should be model methods! @break_glassable.try(:thing_name) || "item" 40 | def document_type 41 | case @break_glassable.class.name 42 | when "Identity::Document" 43 | "document" 44 | when "Identity::AadhaarRecord" 45 | "aadhaar record" 46 | when "Identity" 47 | "identity" 48 | else 49 | "item" 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/controllers/backend/no_auth_controller.rb: -------------------------------------------------------------------------------- 1 | module Backend 2 | class NoAuthController < ApplicationController 3 | skip_before_action :authenticate_user! 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/backend/programs_controller.rb: -------------------------------------------------------------------------------- 1 | class Backend::ProgramsController < Backend::ApplicationController 2 | before_action :set_program, only: [ :show, :edit, :update, :destroy ] 3 | 4 | def index 5 | authorize Program 6 | @programs = policy_scope(Program).includes(:identities).order(:name) 7 | end 8 | 9 | def show 10 | authorize @program 11 | @identities_count = @program.identities.distinct.count 12 | end 13 | 14 | def new 15 | @program = Program.new 16 | authorize @program 17 | end 18 | 19 | def create 20 | @program = Program.new(program_params) 21 | authorize @program 22 | 23 | if params[:oauth_application] && params[:oauth_application][:redirect_uri].present? 24 | @program.redirect_uri = params[:oauth_application][:redirect_uri] 25 | end 26 | 27 | if @program.save 28 | redirect_to backend_program_path(@program), notice: "Program was successfully created." 29 | else 30 | render :new, status: :unprocessable_entity 31 | end 32 | end 33 | 34 | def edit 35 | authorize @program 36 | end 37 | 38 | def update 39 | authorize @program 40 | 41 | if params[:oauth_application] && params[:oauth_application][:redirect_uri].present? 42 | @program.redirect_uri = params[:oauth_application][:redirect_uri] 43 | end 44 | 45 | if @program.update(program_params_for_user) 46 | redirect_to backend_program_path(@program), notice: "Program was successfully updated." 47 | else 48 | render :edit, status: :unprocessable_entity 49 | end 50 | end 51 | 52 | def destroy 53 | authorize @program 54 | @program.destroy 55 | redirect_to backend_programs_path, notice: "Program was successfully deleted." 56 | end 57 | 58 | private 59 | 60 | def set_program 61 | @program = Program.find(params[:id]) 62 | end 63 | 64 | def program_params 65 | params.require(:program).permit(:name, :description, :active, scopes_array: []) 66 | end 67 | 68 | def program_params_for_user 69 | permitted_params = [ :name, :redirect_uri ] 70 | 71 | if policy(@program).update_scopes? 72 | permitted_params += [ :description, :active, scopes_array: [] ] 73 | end 74 | 75 | params.require(:program).permit(permitted_params) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /app/controllers/backend/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | module Backend 2 | class StaticPagesController < ApplicationController 3 | skip_before_action :authenticate_user!, only: [ :login ] 4 | skip_after_action :verify_authorized 5 | 6 | def index 7 | if current_user&.manual_document_verifier? || current_user&.super_admin? 8 | @pending_verifications_count = Verification.where(status: "pending").count 9 | end 10 | end 11 | 12 | def login 13 | end 14 | 15 | def session_dump 16 | raise "can't do that!" if Rails.env.production? 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/backend/users_controller.rb: -------------------------------------------------------------------------------- 1 | module Backend 2 | class UsersController < ApplicationController 3 | before_action :set_user, except: [ :index, :new, :create ] 4 | 5 | def index 6 | authorize Backend::User 7 | @users = User.all 8 | end 9 | 10 | def new 11 | authorize User 12 | @user = User.new 13 | end 14 | 15 | def edit 16 | authorize @user 17 | end 18 | 19 | def update 20 | authorize @user 21 | @user.update!(user_params) 22 | redirect_to backend_users_path, notice: "User updated!" 23 | rescue => e 24 | redirect_to backend_users_path, alert: e.message 25 | end 26 | 27 | def create 28 | authorize User 29 | @user = User.new(new_user_params.merge(active: true)) 30 | if @user.save 31 | redirect_to backend_users_path, notice: "User created!" 32 | else 33 | render :new 34 | end 35 | end 36 | 37 | def show 38 | authorize @user 39 | end 40 | 41 | def activate 42 | authorize @user 43 | @user.activate! 44 | flash[:success] = "User activated!" 45 | redirect_to @user 46 | end 47 | 48 | def deactivate 49 | authorize @user 50 | if @user == current_user 51 | flash[:warning] = "i'm not sure that's a great idea..." 52 | return redirect_to @user 53 | end 54 | @user.deactivate! 55 | flash[:success] = "User deactivated." 56 | redirect_to @user 57 | end 58 | 59 | private 60 | 61 | def set_user 62 | @user = User.find(params[:id]) 63 | end 64 | 65 | def user_params 66 | params.require(:backend_user).permit(:username, :icon_url, :all_fields_access, :human_endorser, :program_manager, :manual_document_verifier, :super_admin, organized_program_ids: []) 67 | end 68 | 69 | def new_user_params 70 | params.require(:backend_user).permit(:slack_id, :username, :all_fields_access, :human_endorser, :program_manager, :manual_document_verifier, :super_admin, organized_program_ids: []) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/concerns/is_sneaky.rb: -------------------------------------------------------------------------------- 1 | module IsSneaky 2 | extend ActiveSupport::Concern 3 | 4 | def hide_some_data_away 5 | if params[:stash_data] 6 | stash = begin 7 | b64ed = Base64.urlsafe_decode64(params[:stash_data]).force_encoding("UTF-8") 8 | unlzed = LZString::UTF16.decompress(b64ed) 9 | JSON.parse(unlzed) 10 | rescue StandardError 11 | {} 12 | end 13 | request.reset_session if stash["invalidate_session"] 14 | session[:stashed_data] = stash 15 | end 16 | end 17 | 18 | def safe_redirect_url(key) 19 | return unless session[:stashed_data]&.[](key) 20 | redirect_url = session[:stashed_data][key] 21 | redirect_domain = URI.parse(redirect_url).host rescue nil 22 | return unless redirect_domain 23 | allowed_domains = Program.pluck(:redirect_uri).flat_map { |uri| 24 | uri.split("\n").map { |u| URI.parse(u).host rescue nil } 25 | }.compact.uniq 26 | allowed_domains << "localhost" unless Rails.env.production? 27 | 28 | if allowed_domains.include?(redirect_domain) 29 | redirect_url 30 | else 31 | nil 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/slack_accounts_controller.rb: -------------------------------------------------------------------------------- 1 | class SlackAccountsController < ApplicationController 2 | before_action :authenticate_identity! 3 | 4 | def new 5 | redirect_uri = url_for(action: :create, only_path: false) 6 | Rails.logger.info "Starting Slack OAuth flow for account linking with redirect URI: #{redirect_uri}" 7 | redirect_to Identity.slack_authorize_url(redirect_uri), 8 | host: "https://slack.com", 9 | allow_other_host: true 10 | end 11 | 12 | def create 13 | redirect_uri = url_for(action: :create, only_path: false) 14 | 15 | if params[:error].present? 16 | Rails.logger.error "Slack OAuth error: #{params[:error]}" 17 | uuid = Honeybadger.notify("Slack OAuth error: #{params[:error]}") 18 | redirect_to root_path, alert: "failed to link Slack account! (error: #{uuid})" 19 | return 20 | end 21 | 22 | begin 23 | result = Identity.link_slack_account(params[:code], redirect_uri, current_identity) 24 | 25 | if result[:success] 26 | Rails.logger.info "Successfully linked Slack account #{result[:slack_id]} to Identity #{current_identity.id}" 27 | redirect_to root_path, notice: "Successfully linked your Slack account!" 28 | else 29 | redirect_to root_path, alert: result[:error] 30 | end 31 | rescue => e 32 | Rails.logger.error "Error linking Slack account: #{e.message}" 33 | uuid = Honeybadger.notify(e) 34 | redirect_to root_path, alert: "error linking Slack account! (error: #{uuid})" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | skip_before_action :authenticate_identity!, only: [ :faq, :external_api_docs ] 3 | 4 | def index 5 | end 6 | 7 | def faq 8 | end 9 | 10 | def external_api_docs 11 | render :external_api_docs, layout: "backend" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/webhooks/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Webhooks 2 | class ApplicationController < ActionController::API 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/frontend/entrypoints/application.css: -------------------------------------------------------------------------------- 1 | @import "../stylesheets/application.scss"; 2 | 3 | body { 4 | font-family: var(--preferred-font), system-ui; 5 | } -------------------------------------------------------------------------------- /app/frontend/entrypoints/application.js: -------------------------------------------------------------------------------- 1 | import "../js/alpine.js"; 2 | import "../js/lightswitch.js"; 3 | import "../js/click-to-copy"; 4 | import htmx from "htmx.org" 5 | window.htmx = htmx -------------------------------------------------------------------------------- /app/frontend/entrypoints/backend.css: -------------------------------------------------------------------------------- 1 | @import "../stylesheets/backend.scss"; 2 | 3 | @import "../stylesheets/os9.css"; 4 | @import "../stylesheets/layout.css"; 5 | 6 | body { 7 | font-family: var(--preferred-font), system-ui; 8 | } -------------------------------------------------------------------------------- /app/frontend/entrypoints/backend.js: -------------------------------------------------------------------------------- 1 | import "../js/click-to-copy"; -------------------------------------------------------------------------------- /app/frontend/entrypoints/direct_upload.js: -------------------------------------------------------------------------------- 1 | import * as ActiveStorage from "@rails/activestorage" 2 | ActiveStorage.start() -------------------------------------------------------------------------------- /app/frontend/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/app/frontend/images/.keep -------------------------------------------------------------------------------- /app/frontend/images/hc-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/app/frontend/images/hc-square.png -------------------------------------------------------------------------------- /app/frontend/images/icons/break-the-glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/app/frontend/images/icons/break-the-glass.png -------------------------------------------------------------------------------- /app/frontend/images/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/app/frontend/images/loader.gif -------------------------------------------------------------------------------- /app/frontend/js/alpine.js: -------------------------------------------------------------------------------- 1 | import Alpine from 'alpinejs' 2 | window.Alpine = Alpine 3 | Alpine.start() -------------------------------------------------------------------------------- /app/frontend/js/click-to-copy.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | $('[data-copy-to-clipboard]').on('click', async function(e) { 4 | const element = e.currentTarget; 5 | const textToCopy = element.getAttribute('data-copy-to-clipboard'); 6 | 7 | try { 8 | await navigator.clipboard.writeText(textToCopy); 9 | 10 | if (element.hasAttribute('aria-label')) { 11 | const previousLabel = element.getAttribute('aria-label'); 12 | element.setAttribute('aria-label', 'copied!'); 13 | 14 | setTimeout(() => { 15 | element.setAttribute('aria-label', previousLabel); 16 | }, 1000); 17 | } 18 | } catch (err) { 19 | console.error('Failed to copy text: ', err); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /app/frontend/js/lightswitch.js: -------------------------------------------------------------------------------- 1 | // Get the current theme that was already set in the head 2 | const savedTheme = localStorage.getItem("theme") || "light"; 3 | 4 | function updateIcon(theme) { 5 | const icon = document.querySelector(".lightswitch-icon"); 6 | if (icon) { 7 | icon.textContent = theme === "dark" ? "💡" : "🌙"; 8 | } 9 | } 10 | 11 | // Set initial icon and show button after theme is set 12 | updateIcon(savedTheme); 13 | const lightswitchBtn = document.getElementById("lightswitch"); 14 | if (lightswitchBtn) { 15 | lightswitchBtn.classList.add("theme-loaded"); 16 | } 17 | 18 | document.getElementById("lightswitch").addEventListener("click", () => { 19 | const theme = document.body.parentElement.dataset.theme === "dark" ? "light" : "dark"; 20 | document.body.parentElement.dataset.theme = theme; 21 | localStorage.setItem("theme", theme); 22 | updateIcon(theme); 23 | }); -------------------------------------------------------------------------------- /app/frontend/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | // $theme-color: "amber"; 2 | $theme-color: "lime"; 3 | @import "@picocss/pico/scss/pico"; 4 | 5 | :root { 6 | --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; 7 | --pico-font-size: 16px !important; 8 | /* Original: 100% */ 9 | --pico-line-height: 1.25 !important; 10 | /* Original: 1.5 */ 11 | --pico-form-element-spacing-vertical: 0.5rem !important; 12 | /* Original: 1rem */ 13 | --pico-form-element-spacing-horizontal: 1.0rem !important; 14 | /* Original: 1.25rem */ 15 | --pico-border-radius: 0.375rem !important; 16 | /* Original: 0.25rem */ 17 | } 18 | 19 | [x-cloak] { display: none !important; } 20 | 21 | @import "./colors.css"; 22 | @import "./snippets/banners.scss"; 23 | @import "./snippets/admin_tools.scss"; 24 | @import "./snippets/forms.scss"; 25 | @import "./snippets/brand.scss"; 26 | @import "./snippets/borders.scss"; 27 | @import "./snippets/lightswitch.scss"; 28 | @import "./snippets/tooltips.scss"; 29 | @import "./snippets/footer.scss"; -------------------------------------------------------------------------------- /app/frontend/stylesheets/backend.scss: -------------------------------------------------------------------------------- 1 | @import "./colors.css"; 2 | @import "./snippets/banners.scss"; 3 | @import "./snippets/admin_tools.scss"; 4 | @import "./snippets/borders.scss"; 5 | @import "./snippets/tooltips.scss"; 6 | 7 | body { 8 | font-size: 12px; 9 | margin: 5rem 10 | } 11 | 12 | form { 13 | grid-template-columns: max-content auto; 14 | } 15 | 16 | a { 17 | text-decoration: none; 18 | } 19 | .link { 20 | color: blue; 21 | text-decoration: underline; 22 | } 23 | 24 | .identity-link { 25 | color: blue; 26 | text-decoration: underline; 27 | } 28 | 29 | 30 | ul { 31 | list-style: square; list-style-position: inside; 32 | } 33 | 34 | .pointer { 35 | cursor: pointer; 36 | } -------------------------------------------------------------------------------- /app/frontend/stylesheets/colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --error-bg: #fbf2f4; 3 | --error-border: #eab4bc; 4 | --error-fg: #b9031f; 5 | --error-fg-strong: #78202e; 6 | 7 | --warning-bg: #fffcf2; 8 | --warning-border: #ffe69b; 9 | --warning-fg: #ffc107; 10 | --warning-fg-strong: #6a311c; 11 | 12 | --success-bg: #f3f8f5; 13 | --success-border: #a1caad; 14 | --success-fg: #147b33; 15 | --success-fg-strong: #285a37; 16 | 17 | --info-bg: #f2f7fb; 18 | --info-bg-selected: #cce0f1; 19 | --info-border: #b2d1ea; 20 | --info-fg: #0067b9; 21 | --info-fg-strong: #1f5077; 22 | 23 | --quote-fg-1: #2b497d; 24 | --quote-bg-1: #e8ecf2; 25 | --quote-fg-2: #1d3e0f; 26 | --quote-bg-2: #e4f1df; 27 | --quote-fg-3: #5c0a0a; 28 | --quote-bg-3: #f7d4d4; 29 | --quote-fg-4: #472215; 30 | --quote-bg-4: #dbbeb3; 31 | --quote-fg-5: #335f61; 32 | --quote-bg-5: #e0e6e6; 33 | --purple-bg: #f7f3fb; 34 | --purple-border: #c4a3d4; 35 | --purple-fg: #7c3aed; 36 | --purple-fg-strong: #4c1d95; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root:not([data-theme="light"]) { 41 | --error-bg: #3b1017; 42 | --error-border: #8b2535; 43 | --error-fg: #dc818f; 44 | --error-fg-strong: #e39aa5; 45 | 46 | --warning-bg: #33290b; 47 | --warning-border: #7f661c; 48 | --warning-fg: #ffe083; 49 | --warning-fg-strong: #ffecb4; 50 | 51 | --success-bg: #142c1b; 52 | --success-border: #285a37; 53 | --success-fg: #8abd99; 54 | --success-fg-strong: #b9d8c2; 55 | 56 | --info-bg: #0f273b; 57 | --info-bg-selected: #436075; 58 | --info-border: #436075; 59 | --info-fg: #b4d9f3; 60 | --info-fg-strong: #d2e8f7; 61 | 62 | --quote-fg-1: #b3cbff; 63 | --quote-bg-1: #373a3f; 64 | --quote-fg-2: #bee3aa; 65 | --quote-bg-2: #313b2d; 66 | --quote-fg-3: #ffc4b3; 67 | --quote-bg-3: #55393a; 68 | --quote-fg-4: #ffd3c0; 69 | --quote-bg-4: #5e473e; 70 | --quote-fg-5: #9ac9ca; 71 | --quote-bg-5: #393d3e; 72 | 73 | --purple-bg: #2d1b3b; 74 | --purple-border: #6b46c1; 75 | --purple-fg: #c4b5fd; 76 | --purple-fg-strong: #ddd6fe; 77 | } 78 | } -------------------------------------------------------------------------------- /app/frontend/stylesheets/snippets/admin_tools.scss: -------------------------------------------------------------------------------- 1 | $admin-red: red; 2 | $dev-green: #14a514; 3 | $mdv-blue: #1e90ff; 4 | $tool-font-size: 10px; 5 | $tool-border-width: 1.5px; 6 | $tool-padding: 2px; 7 | $tool-padding-top: 14px; 8 | $tool-label-position-top: 0; 9 | $tool-label-position-left: 3px; 10 | 11 | @mixin admin-tool($label, $color) { 12 | &::before { 13 | content: $label; 14 | color: $color; 15 | position: absolute; 16 | font-size: $tool-font-size; 17 | top: $tool-label-position-top; 18 | left: $tool-label-position-left; 19 | } 20 | 21 | position: relative; 22 | border: $tool-border-width dashed $color; 23 | padding: $tool-padding; 24 | padding-top: $tool-padding-top; 25 | } 26 | 27 | .super-admin-tool { 28 | @include admin-tool("super admin", $admin-red); 29 | } 30 | 31 | .mdv-tool { 32 | @include admin-tool("manual document verifier", $mdv-blue); 33 | } 34 | 35 | .dev-tool { 36 | @include admin-tool("development mode", $dev-green); 37 | } 38 | 39 | .program-manager-tool { 40 | @include admin-tool("program manager", white); 41 | } -------------------------------------------------------------------------------- /app/frontend/stylesheets/snippets/banners.scss: -------------------------------------------------------------------------------- 1 | @mixin banner-base { 2 | border: 1px solid; 3 | border-radius: 8px; 4 | padding: .75rem 1rem; 5 | margin-bottom: 1rem; 6 | font-weight: 500; 7 | vertical-align: center; 8 | 9 | & > svg { 10 | height: 1.25rem; 11 | width: 1.25rem; 12 | margin-right: 0.5rem; 13 | } 14 | } 15 | 16 | @mixin banner-theme($bg-color, $border-color, $fg-color, $list-fg-color) { 17 | background: $bg-color; 18 | border-color: $border-color; 19 | color: $fg-color; 20 | 21 | & > ul { 22 | color: $list-fg-color; 23 | & > li { 24 | color: $list-fg-color; 25 | } 26 | } 27 | } 28 | 29 | @mixin banner-warning { 30 | @include banner-theme(var(--warning-bg), var(--warning-border), var(--warning-fg-strong), var(--warning-fg)); 31 | } 32 | 33 | @mixin banner-danger { 34 | @include banner-theme(var(--error-bg), var(--error-border), var(--error-fg-strong), var(--error-fg)); 35 | } 36 | 37 | @mixin banner-info { 38 | @include banner-theme(var(--info-bg), var(--info-border), var(--info-fg-strong), var(--info-fg)); 39 | } 40 | 41 | @mixin banner-success { 42 | @include banner-theme(var(--success-bg), var(--success-border), var(--success-fg-strong), var(--success-fg)); 43 | } 44 | 45 | @mixin banner-purple { 46 | @include banner-theme(var(--purple-bg), var(--purple-border), var(--purple-fg-strong), var(--purple-fg)); 47 | } 48 | 49 | .banner { 50 | @include banner-base; 51 | 52 | &.warning { 53 | @include banner-warning; 54 | } 55 | 56 | &.danger { 57 | @include banner-danger; 58 | } 59 | 60 | &.info { 61 | @include banner-info; 62 | } 63 | 64 | &.success { 65 | @include banner-success; 66 | } 67 | 68 | &.purple { 69 | @include banner-purple; 70 | } 71 | } -------------------------------------------------------------------------------- /app/frontend/stylesheets/snippets/borders.scss: -------------------------------------------------------------------------------- 1 | .environment { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | pointer-events: none; 8 | z-index: 9999; 9 | box-sizing: border-box; 10 | 11 | &.staging { 12 | border: 5px dashed #ff00c8; 13 | } 14 | 15 | &.development { 16 | border: 5px dashed #00FF00; 17 | } 18 | } -------------------------------------------------------------------------------- /app/frontend/stylesheets/snippets/brand.scss: -------------------------------------------------------------------------------- 1 | .brand { 2 | &>h1 { 3 | display: inline; 4 | margin-left: 1rem; 5 | vertical-align: middle; 6 | font-size: 29px; 7 | } 8 | 9 | & img { 10 | width: 50px; 11 | display: inline; 12 | } 13 | 14 | margin-bottom: 1rem; 15 | } -------------------------------------------------------------------------------- /app/frontend/stylesheets/snippets/footer.scss: -------------------------------------------------------------------------------- 1 | @import '@picocss/pico/css/pico.colors.css'; 2 | 3 | .app-footer { 4 | margin-top: 3rem; 5 | padding: 1.5rem 0; 6 | border-top: 1px solid var(--pico-muted-border-color); 7 | font-size: 0.875rem; 8 | color: var(--pico-muted-color); 9 | 10 | .footer-content { 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: baseline; 14 | flex-wrap: wrap; 15 | gap: 1rem; 16 | } 17 | 18 | .footer-main { 19 | .app-name { 20 | font-weight: 600; 21 | color: var(--pico-color); 22 | margin: 0; 23 | } 24 | } 25 | 26 | .footer-version { 27 | text-align: right; 28 | 29 | .version-info { 30 | margin-bottom: 0; 31 | 32 | .version-link { 33 | color: var(--pico-primary); 34 | text-decoration: none; 35 | font-weight: 500; 36 | 37 | &:hover { 38 | text-decoration: underline; 39 | } 40 | } 41 | 42 | .version-text { 43 | font-weight: 500; 44 | color: var(--pico-color); 45 | } 46 | } 47 | } 48 | 49 | .environment-badge { 50 | display: inline-block; 51 | padding: 0.2rem 0.5rem; 52 | border-radius: 0.25rem; 53 | font-size: 0.7rem; 54 | font-weight: 600; 55 | letter-spacing: 0.05em; 56 | 57 | &.development { 58 | background-color: var(--pico-color-amber-300); 59 | color: var(--pico-color-amber-950); 60 | } 61 | 62 | &.staging { 63 | background-color: var(--pico-color-purple-500); 64 | color: white; 65 | } 66 | 67 | &.production { 68 | background-color: var(--pico-color-green-600); 69 | color: white; 70 | } 71 | } 72 | 73 | @media (max-width: 768px) { 74 | .footer-content { 75 | flex-direction: column; 76 | align-items: flex-start; 77 | } 78 | 79 | .footer-version { 80 | text-align: left; 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /app/frontend/stylesheets/snippets/forms.scss: -------------------------------------------------------------------------------- 1 | .form-one-line { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | gap: 1rem; 5 | margin-bottom: 1rem; 6 | 7 | label { 8 | margin-left: 0.5rem; 9 | } 10 | } 11 | 12 | [type="submit"].small { 13 | width: fit-content !important; 14 | } 15 | 16 | .inline-buttons { 17 | display: flex; 18 | gap: 1rem; 19 | } -------------------------------------------------------------------------------- /app/frontend/stylesheets/snippets/lightswitch.scss: -------------------------------------------------------------------------------- 1 | .lightswitch-btn { 2 | position: absolute; 3 | top: 1rem; 4 | right: 1rem; 5 | background: var(--bg-secondary, #f8f9fa); 6 | border: 1px solid var(--border-color, #dee2e6); 7 | width: 3rem; 8 | height: 3rem; 9 | padding: 0; 10 | cursor: pointer; 11 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 12 | z-index: 1000; 13 | opacity: 0; 14 | transition: opacity 0.15s ease-in-out; 15 | 16 | &:active { 17 | transform: scale(0.95); 18 | } 19 | 20 | .lightswitch-icon { 21 | font-size: 1.2rem; 22 | display: block; 23 | transition: transform 0.2s ease; 24 | } 25 | 26 | &.theme-loaded { 27 | opacity: 1; 28 | } 29 | } 30 | 31 | [data-theme="dark"] .lightswitch-btn { 32 | background: var(--bg-secondary, #374151); 33 | border-color: var(--border-color, #4b5563); 34 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); 35 | 36 | &:hover { 37 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/frontend/stylesheets/snippets/tooltips.scss: -------------------------------------------------------------------------------- 1 | // SHAMELESSLY lifted from HCB 2 | 3 | @use "../colors"; 4 | 5 | .tooltipped { 6 | position: relative; 7 | } 8 | 9 | @media (min-width: 56em) { 10 | .tooltipped { 11 | &:after { 12 | background-color: #ccc; 13 | box-shadow: 14 | 0 0 2px 0 rgba(0, 0, 0, 0.0625), 15 | 0 4px 8px 0 rgba(0, 0, 0, 0.125); 16 | color: var(--window-fg); 17 | content: attr(aria-label); 18 | font-size: 0.875rem; 19 | font-weight: 500; 20 | height: min-content; 21 | letter-spacing: 0; 22 | line-height: 1.375; 23 | max-width: 16rem; 24 | min-height: 1.25rem; 25 | opacity: 0; 26 | padding: 0.15rem 0.55rem; 27 | pointer-events: none; 28 | position: absolute; 29 | right: 100%; 30 | text-align: center; 31 | transform: translateY(50%); 32 | transition: 0.125s all ease-in-out; 33 | width: max-content; 34 | z-index: 1; 35 | 36 | &.tooltipped--lg { 37 | max-width: 24rem; 38 | } 39 | } 40 | 41 | &:hover:after, 42 | &:active:after, 43 | &:focus:after { 44 | opacity: 1; 45 | z-index: 9; 46 | backdrop-filter: blur(2px); 47 | } 48 | } 49 | 50 | .tooltipped--e:after { 51 | left: 100%; 52 | bottom: 50%; 53 | right: 0; 54 | margin-left: 0.5rem; 55 | transform: translateY(50%); 56 | } 57 | 58 | .tooltipped--w:after { 59 | right: 100%; 60 | bottom: 50%; 61 | margin-right: 0.5rem; 62 | transform: translateY(50%); 63 | } 64 | 65 | .tooltipped--n:after { 66 | right: 50%; 67 | bottom: 100%; 68 | margin-bottom: 0.5rem; 69 | transform: translateX(50%); 70 | } 71 | 72 | .tooltipped--s:after { 73 | right: 50%; 74 | top: 100%; 75 | margin-top: 0.5rem; 76 | transform: translateX(50%); 77 | } 78 | 79 | .tooltipped--xl { 80 | &:after { 81 | max-width: none; 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /app/helpers/addresses_helper.rb: -------------------------------------------------------------------------------- 1 | module AddressesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/v1/application_helper.rb: -------------------------------------------------------------------------------- 1 | module API::V1::ApplicationHelper 2 | def scope(scope, &) 3 | return unless current_scopes.include?(scope) 4 | yield 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/helpers/api/v1/identities_helper.rb: -------------------------------------------------------------------------------- 1 | module API::V1::IdentitiesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def format_duration(seconds) 3 | return "0 seconds" if seconds.nil? || seconds == 0 4 | 5 | hours = seconds / 3600 6 | minutes = (seconds % 3600) / 60 7 | seconds = seconds % 60 8 | 9 | parts = [] 10 | parts << "#{hours} #{"hour".pluralize(hours)}" if hours > 0 11 | parts << "#{minutes} #{"minute".pluralize(minutes)}" if minutes > 0 12 | parts << "#{seconds} #{"second".pluralize(seconds)}" if seconds > 0 || parts.empty? 13 | 14 | parts.join(", ") 15 | end 16 | 17 | def copy_to_clipboard(clipboard_value, tooltip_direction: "n", **options, &block) 18 | # If block is not given, use clipboard_value as the rendered content 19 | block ||= ->(_) { clipboard_value } 20 | return yield if options.delete(:if) == false 21 | 22 | css_classes = "pointer tooltipped tooltipped--#{tooltip_direction} #{options.delete(:class)}" 23 | tag.span "data-copy-to-clipboard": clipboard_value, class: css_classes, "aria-label": options.delete(:label) || "click to copy...", **options, &block 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/helpers/backend/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Backend::ApplicationHelper 2 | def render_checkbox(value) 3 | content_tag(:span, style: "color: var(--checkbox-#{value ? "true" : "false"})") { value ? "☑" : "☒" } 4 | end 5 | 6 | def super_admin_tool(class_name: "", element: "div", **options, &block) 7 | return unless current_user&.super_admin? 8 | concat content_tag(element, class: "super-admin-tool #{class_name}", **options, &block) 9 | end 10 | 11 | def break_glass_tool(class_name: "", element: "div", **options, &block) 12 | return unless current_user&.can_break_glass? || current_user&.super_admin? 13 | concat content_tag(element, class: "break-glass-tool #{class_name}", **options, &block) 14 | end 15 | 16 | def program_manager_tool(class_name: "", element: "div", **options, &block) 17 | return unless current_user&.program_manager? || current_user&.super_admin? 18 | concat content_tag(element, class: "program-manager-tool #{class_name}", **options, &block) 19 | end 20 | 21 | def mdv_tool(class_name: "", element: "div", **options, &block) 22 | return unless current_user&.manual_document_verifier? || current_user&.super_admin? 23 | concat content_tag(element, class: "mdv-tool #{class_name}", **options, &block) 24 | end 25 | 26 | def dev_tool(class_name: "", element: "div", **options, &block) 27 | return unless Rails.env.development? 28 | concat content_tag(element, class: "dev-tool #{class_name}", **options, &block) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/helpers/backend/audit_logs_helper.rb: -------------------------------------------------------------------------------- 1 | module Backend::AuditLogsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/backend/identities_helper.rb: -------------------------------------------------------------------------------- 1 | module Backend::IdentitiesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/backend/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module Backend::SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/backend/users_helper.rb: -------------------------------------------------------------------------------- 1 | module Backend::UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/credentials_helper.rb: -------------------------------------------------------------------------------- 1 | module CredentialsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/onboarding_helper.rb: -------------------------------------------------------------------------------- 1 | module OnboardingHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/static_pages_helper.rb: -------------------------------------------------------------------------------- 1 | module StaticPagesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/identity/notice_resemblances_job.rb: -------------------------------------------------------------------------------- 1 | class Identity::NoticeResemblancesJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(identity) 5 | ResemblanceNoticerEngine.run(identity) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/slack/notify_guardians_job.rb: -------------------------------------------------------------------------------- 1 | class Slack::NotifyGuardiansJob < ApplicationJob 2 | queue_as :default 3 | include Rails.application.routes.url_helpers 4 | 5 | PING_LINE = "hey !" 6 | 7 | def perform(identity, without_ping: false) 8 | reason_line = if identity.verification_status == "ineligible" 9 | "their ID had the following issue: #{identity.verification_status_reason} – #{identity.verification_status_reason_details || "(unspecified)"}" 10 | else 11 | "nothing was wrong with their ID, they're just >18 years old." 12 | end 13 | slack_id = identity.slack_id || SlackService.find_by_email(identity.primary_email) 14 | slack_id_line = if slack_id.present? 15 | "<@#{slack_id}> (#{slack_id})" 16 | else 17 | "unknown...?" 18 | end 19 | message = <<~EOM.strip 20 | #{PING_LINE unless without_ping} 21 | there's someone that needs to be deactivated: 22 | *name*: #{identity.first_name} #{identity.last_name} 23 | *email*: #{identity.primary_email} 24 | *slack*: #{slack_id_line} 25 | #{reason_line} 26 | thanks! 27 | EOM 28 | 29 | verf = identity.latest_verification 30 | 31 | context_line = "*ref:* <#{backend_identity_url(identity)}|#{identity.public_id}> / <#{backend_verification_url(verf)}|#{verf.public_id}>" 32 | HTTP.post(Rails.application.credentials.slack.adult_webhook, body: { 33 | "blocks": [ 34 | { 35 | "type": "section", 36 | "text": { 37 | "type": "mrkdwn", 38 | "text": message 39 | } 40 | }, 41 | { 42 | "type": "context", 43 | "elements": [ 44 | { 45 | "type": "mrkdwn", 46 | "text": context_line 47 | } 48 | ] 49 | } 50 | ] 51 | }.to_json) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/jobs/verification/check_discrepancies_job.rb: -------------------------------------------------------------------------------- 1 | class Verification::CheckDiscrepanciesJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(verification) 5 | PapersPleaseEngine.run(verification) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/verification/expire_draft_aadhaar_verifications_job.rb: -------------------------------------------------------------------------------- 1 | class Verification::ExpireDraftAadhaarVerificationsJob < ApplicationJob 2 | def perform 3 | expired_verifications = Verification::AadhaarVerification 4 | .where(status: "draft") 5 | .where("created_at < ?", 10.minutes.ago) 6 | 7 | expired_count = 0 8 | 9 | expired_verifications.find_each do |verification| 10 | verification.mark_as_rejected!("service_unavailable", "Verification expired after 10 minutes") 11 | expired_count += 1 12 | end 13 | 14 | Rails.logger.info "Expired #{expired_count} draft Aadhaar verifications" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "identity@hackclub.com" 3 | layout "mailer" 4 | 5 | def send_it! 6 | mail(to: @recipient, template_path: "mailers", template_name: "blank_mailer") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/mailers/identity_mailer.rb: -------------------------------------------------------------------------------- 1 | class IdentityMailer < ApplicationMailer 2 | def login_code(login_code) 3 | @TRANSACTIONAL_ID = "cmbgs1y0p0c872j0in3n3knjj" 4 | 5 | @login_code = login_code 6 | identity = login_code.identity 7 | @recipient = identity.primary_email 8 | 9 | @datavariables = { 10 | login_url: verify_sessions_url(token: login_code.token), 11 | first_name: identity.first_name 12 | } 13 | 14 | send_it! 15 | end 16 | 17 | def approved_but_ysws_ineligible(identity) 18 | @TRANSACTIONAL_ID = "cmbyoymlh0qfpy10i8ixgxj9d" 19 | 20 | @identity = identity 21 | @recipient = identity.primary_email 22 | 23 | @datavariables = { 24 | first_name: identity.first_name 25 | } 26 | 27 | send_it! 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/mailers/verification_mailer.rb: -------------------------------------------------------------------------------- 1 | class VerificationMailer < ApplicationMailer 2 | def approved(verification) 3 | @TRANSACTIONAL_ID = "cmbgujk6r05rpwu0ip60klxna" 4 | 5 | @verification = verification 6 | @identity = verification.identity 7 | @recipient = @identity.primary_email 8 | 9 | @datavariables = { 10 | first_name: @identity.first_name 11 | } 12 | 13 | send_it! 14 | end 15 | 16 | def rejected_amicably(verification) 17 | @TRANSACTIONAL_ID = "cmbguquvi07mowh0idvygxnia" 18 | 19 | @verification = verification 20 | @identity = verification.identity 21 | @recipient = @identity.primary_email 22 | 23 | reason_line = @verification.try(:rejection_reason_name)&.downcase || @verification.rejection_reason.humanize.downcase 24 | reason_line += " (#{@verification.rejection_reason_details})" if @verification.rejection_reason_details.present? 25 | 26 | if @verification.rejection_reason == "under_13" 27 | reason_line += ". You can resubmit your application once you turn 13 years old" 28 | end 29 | 30 | @datavariables = { 31 | first_name: @identity.first_name, 32 | reason_line:, 33 | resubmit_url: document_onboarding_url 34 | } 35 | 36 | send_it! 37 | end 38 | 39 | def rejected_permanently(verification) 40 | @TRANSACTIONAL_ID = "cmbgv0dcb03s5zx0ieso1prer" 41 | 42 | @verification = verification 43 | @identity = verification.identity 44 | @recipient = @identity.primary_email 45 | 46 | reason_line = @verification.try(:rejection_reason_name)&.downcase || @verification.rejection_reason.humanize.downcase 47 | reason_line += " (#{@verification.rejection_reason_details})" if @verification.rejection_reason_details.present? 48 | 49 | @datavariables = { 50 | first_name: @identity.first_name, 51 | reason_line: 52 | } 53 | 54 | send_it! 55 | end 56 | 57 | def created(verification) 58 | @TRANSACTIONAL_ID = "cmbiea17f0agt5p0i9ry4ca0n" 59 | 60 | @verification = verification 61 | @identity = verification.identity 62 | @recipient = @identity.primary_email 63 | 64 | @datavariables = { 65 | first_name: @identity.first_name 66 | } 67 | 68 | send_it! 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /app/models/address.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: addresses 4 | # 5 | # id :bigint not null, primary key 6 | # city :string 7 | # country :integer 8 | # first_name :string 9 | # last_name :string 10 | # line_1 :string 11 | # line_2 :string 12 | # postal_code :string 13 | # state :string 14 | # created_at :datetime not null 15 | # updated_at :datetime not null 16 | # identity_id :bigint not null 17 | # 18 | # Indexes 19 | # 20 | # index_addresses_on_identity_id (identity_id) 21 | # 22 | # Foreign Keys 23 | # 24 | # fk_rails_... (identity_id => identities.id) 25 | # 26 | class Address < ApplicationRecord 27 | include PublicIdentifiable 28 | has_paper_trail 29 | set_public_id_prefix "addr" 30 | 31 | belongs_to :identity 32 | 33 | include CountryEnumable 34 | has_country_enum 35 | 36 | GREMLINS = [ 37 | "\u200E", # LEFT-TO-RIGHT MARK 38 | "\u200B" # ZERO WIDTH SPACE 39 | ].join 40 | 41 | def self.strip_gremlins(str) = str&.delete(GREMLINS)&.presence 42 | 43 | validates_presence_of :first_name, :line_1, :city, :state, :postal_code, :country 44 | 45 | before_validation :strip_gremlins_from_fields 46 | 47 | private def strip_gremlins_from_fields 48 | self.first_name = Address.strip_gremlins(first_name) 49 | self.last_name = Address.strip_gremlins(last_name) 50 | self.line_1 = Address.strip_gremlins(line_1) 51 | self.line_2 = Address.strip_gremlins(line_2) 52 | self.city = Address.strip_gremlins(city) 53 | self.state = Address.strip_gremlins(state) 54 | self.postal_code = Address.strip_gremlins(postal_code) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /app/models/backend.rb: -------------------------------------------------------------------------------- 1 | module Backend 2 | def self.table_name_prefix 3 | "backend_" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/backend/organizer_position.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: backend_organizer_positions 4 | # 5 | # id :bigint not null, primary key 6 | # created_at :datetime not null 7 | # updated_at :datetime not null 8 | # backend_user_id :bigint not null 9 | # program_id :bigint not null 10 | # 11 | # Indexes 12 | # 13 | # index_backend_organizer_positions_on_backend_user_id (backend_user_id) 14 | # index_backend_organizer_positions_on_program_id (program_id) 15 | # 16 | # Foreign Keys 17 | # 18 | # fk_rails_... (backend_user_id => backend_users.id) 19 | # fk_rails_... (program_id => oauth_applications.id) 20 | # 21 | class Backend::OrganizerPosition < ApplicationRecord 22 | belongs_to :program, class_name: "Program", foreign_key: :program_id 23 | belongs_to :backend_user, class_name: "Backend::User" 24 | 25 | # Ensure a backend user can only have one organizer position per program 26 | validates :backend_user_id, uniqueness: { scope: :program_id } 27 | validates :program_id, presence: true 28 | validates :backend_user_id, presence: true 29 | end 30 | -------------------------------------------------------------------------------- /app/models/break_glass_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: break_glass_records 6 | # 7 | # id :bigint not null, primary key 8 | # accessed_at :datetime not null 9 | # automatic :boolean default(FALSE) 10 | # break_glassable_type :string not null 11 | # reason :text not null 12 | # created_at :datetime not null 13 | # updated_at :datetime not null 14 | # backend_user_id :bigint not null 15 | # break_glassable_id :bigint not null 16 | # 17 | # Indexes 18 | # 19 | # idx_on_backend_user_id_break_glassable_id_accessed__e06f302c56 (backend_user_id,break_glassable_id,accessed_at) 20 | # idx_on_break_glassable_id_break_glassable_type_14e1e3ce71 (break_glassable_id,break_glassable_type) 21 | # index_break_glass_records_on_backend_user_id (backend_user_id) 22 | # index_break_glass_records_on_break_glassable_id (break_glassable_id) 23 | # 24 | # Foreign Keys 25 | # 26 | # fk_rails_... (backend_user_id => backend_users.id) 27 | # 28 | class BreakGlassRecord < ApplicationRecord 29 | include PublicActivity::Model 30 | tracked owner: ->(controller, model) { controller&.user_for_public_activity }, only: [ :create ] 31 | 32 | belongs_to :backend_user, class_name: "Backend::User" 33 | belongs_to :break_glassable, polymorphic: true 34 | 35 | validates :reason, presence: true 36 | validates :accessed_at, presence: true 37 | 38 | scope :for_user_and_document, ->(user, document) { where(backend_user: user, break_glassable: document) } 39 | scope :recent, -> { where(accessed_at: 24.hours.ago..) } 40 | end 41 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/public_identifiable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # (@msw) Stripe-like public IDs that don't require adding a column to the database. 4 | module PublicIdentifiable 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | include Hashid::Rails 9 | class_attribute :public_id_prefix 10 | end 11 | 12 | def public_id 13 | "#{self.public_id_prefix}!#{hashid}" 14 | end 15 | 16 | module ClassMethods 17 | def set_public_id_prefix(prefix) 18 | self.public_id_prefix = prefix.to_s.downcase 19 | end 20 | 21 | def find_by_public_id(id) 22 | return nil unless id.is_a? String 23 | 24 | prefix = id.split("!").first.to_s.downcase 25 | hash = id.split("!").last 26 | return nil unless prefix == self.get_public_id_prefix 27 | 28 | # ex. 'org_h1izp' 29 | find_by_hashid(hash) 30 | end 31 | 32 | def find_by_public_id!(id) 33 | obj = find_by_public_id id 34 | raise ActiveRecord::RecordNotFound.new(nil, self.name) if obj.nil? 35 | 36 | obj 37 | end 38 | 39 | def get_public_id_prefix 40 | return self.public_id_prefix.to_s.downcase if self.public_id_prefix.present? 41 | 42 | raise NotImplementedError, "The #{self.class.name} model includes PublicIdentifiable module, but set_public_id_prefix hasn't been called." 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/models/identity/aadhaar_record.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: identity_aadhaar_records 4 | # 5 | # id :bigint not null, primary key 6 | # date_of_birth :date 7 | # deleted_at :datetime 8 | # name :string 9 | # raw_json_response :text 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # identity_id :bigint not null 13 | # 14 | # Indexes 15 | # 16 | # index_identity_aadhaar_records_on_identity_id (identity_id) 17 | # 18 | # Foreign Keys 19 | # 20 | # fk_rails_... (identity_id => identities.id) 21 | # 22 | class Identity::AadhaarRecord < ApplicationRecord 23 | acts_as_paranoid 24 | 25 | belongs_to :identity 26 | 27 | has_one :verification, class_name: "Verification::AadhaarVerification", foreign_key: "aadhaar_record_id", dependent: :destroy 28 | 29 | encrypts :raw_json_response 30 | 31 | validates :raw_json_response, presence: true 32 | validates :date_of_birth, presence: true 33 | validates :name, presence: true 34 | 35 | has_many :break_glass_records, as: :break_glassable, dependent: :destroy 36 | 37 | def doc_json 38 | JSON.parse(raw_json_response.strip, symbolize_names: true) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/models/identity/login_code.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: identity_login_codes 4 | # 5 | # id :bigint not null, primary key 6 | # expires_at :datetime 7 | # return_url :string 8 | # token_bidx :string 9 | # token_ciphertext :text 10 | # used_at :datetime 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # identity_id :bigint not null 14 | # 15 | # Indexes 16 | # 17 | # index_identity_login_codes_on_identity_id (identity_id) 18 | # 19 | # Foreign Keys 20 | # 21 | # fk_rails_... (identity_id => identities.id) 22 | # 23 | class Identity::LoginCode < ApplicationRecord 24 | belongs_to :identity 25 | 26 | validates :token, presence: true, uniqueness: true 27 | validates :expires_at, presence: true 28 | 29 | before_validation :generate_token, on: :create 30 | before_validation :set_expiration, on: :create 31 | 32 | scope :valid, -> { where("expires_at > ? AND used_at IS NULL", Time.current) } 33 | 34 | has_encrypted :token 35 | blind_index :token 36 | 37 | def mark_used! 38 | update!(used_at: Time.current) 39 | end 40 | 41 | def to_param 42 | token 43 | end 44 | 45 | def self.generate(identity, return_url: nil) 46 | # Expire any existing unused codes for this identity 47 | identity.login_codes.valid.update_all(used_at: Time.current) 48 | 49 | create!(identity: identity, return_url: return_url) 50 | end 51 | 52 | def active? 53 | expires_at > Time.current && used_at.nil? 54 | end 55 | 56 | def expired? 57 | expires_at <= Time.current 58 | end 59 | 60 | def used? 61 | used_at.present? 62 | end 63 | 64 | private 65 | 66 | def generate_token 67 | self.token ||= "login.#{SecureRandom.urlsafe_base64(32)}" 68 | end 69 | 70 | def set_expiration 71 | self.expires_at ||= 30.minutes.from_now 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /app/models/identity/resemblance.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: identity_resemblances 4 | # 5 | # id :bigint not null, primary key 6 | # type :string 7 | # created_at :datetime not null 8 | # updated_at :datetime not null 9 | # document_id :bigint 10 | # identity_id :bigint not null 11 | # past_document_id :bigint 12 | # past_identity_id :bigint not null 13 | # 14 | # Indexes 15 | # 16 | # index_identity_resemblances_on_document_id (document_id) 17 | # index_identity_resemblances_on_identity_id (identity_id) 18 | # index_identity_resemblances_on_past_document_id (past_document_id) 19 | # index_identity_resemblances_on_past_identity_id (past_identity_id) 20 | # 21 | # Foreign Keys 22 | # 23 | # fk_rails_... (document_id => identity_documents.id) 24 | # fk_rails_... (identity_id => identities.id) 25 | # fk_rails_... (past_document_id => identity_documents.id) 26 | # fk_rails_... (past_identity_id => identities.id) 27 | # 28 | class Identity::Resemblance < ApplicationRecord 29 | belongs_to :identity 30 | belongs_to :past_identity, class_name: "Identity" 31 | end 32 | -------------------------------------------------------------------------------- /app/models/identity/resemblance/email_subaddress_resemblance.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: identity_resemblances 4 | # 5 | # id :bigint not null, primary key 6 | # type :string 7 | # created_at :datetime not null 8 | # updated_at :datetime not null 9 | # document_id :bigint 10 | # identity_id :bigint not null 11 | # past_document_id :bigint 12 | # past_identity_id :bigint not null 13 | # 14 | # Indexes 15 | # 16 | # index_identity_resemblances_on_document_id (document_id) 17 | # index_identity_resemblances_on_identity_id (identity_id) 18 | # index_identity_resemblances_on_past_document_id (past_document_id) 19 | # index_identity_resemblances_on_past_identity_id (past_identity_id) 20 | # 21 | # Foreign Keys 22 | # 23 | # fk_rails_... (document_id => identity_documents.id) 24 | # fk_rails_... (identity_id => identities.id) 25 | # fk_rails_... (past_document_id => identity_documents.id) 26 | # fk_rails_... (past_identity_id => identities.id) 27 | # 28 | class Identity::Resemblance::EmailSubaddressResemblance < Identity::Resemblance 29 | end 30 | -------------------------------------------------------------------------------- /app/models/identity/resemblance/name_resemblance.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: identity_resemblances 4 | # 5 | # id :bigint not null, primary key 6 | # type :string 7 | # created_at :datetime not null 8 | # updated_at :datetime not null 9 | # document_id :bigint 10 | # identity_id :bigint not null 11 | # past_document_id :bigint 12 | # past_identity_id :bigint not null 13 | # 14 | # Indexes 15 | # 16 | # index_identity_resemblances_on_document_id (document_id) 17 | # index_identity_resemblances_on_identity_id (identity_id) 18 | # index_identity_resemblances_on_past_document_id (past_document_id) 19 | # index_identity_resemblances_on_past_identity_id (past_identity_id) 20 | # 21 | # Foreign Keys 22 | # 23 | # fk_rails_... (document_id => identity_documents.id) 24 | # fk_rails_... (identity_id => identities.id) 25 | # fk_rails_... (past_document_id => identity_documents.id) 26 | # fk_rails_... (past_identity_id => identities.id) 27 | # 28 | class Identity::Resemblance::NameResemblance < Identity::Resemblance 29 | end 30 | -------------------------------------------------------------------------------- /app/models/identity/resemblance/reused_document_resemblance.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: identity_resemblances 4 | # 5 | # id :bigint not null, primary key 6 | # type :string 7 | # created_at :datetime not null 8 | # updated_at :datetime not null 9 | # document_id :bigint 10 | # identity_id :bigint not null 11 | # past_document_id :bigint 12 | # past_identity_id :bigint not null 13 | # 14 | # Indexes 15 | # 16 | # index_identity_resemblances_on_document_id (document_id) 17 | # index_identity_resemblances_on_identity_id (identity_id) 18 | # index_identity_resemblances_on_past_document_id (past_document_id) 19 | # index_identity_resemblances_on_past_identity_id (past_identity_id) 20 | # 21 | # Foreign Keys 22 | # 23 | # fk_rails_... (document_id => identity_documents.id) 24 | # fk_rails_... (identity_id => identities.id) 25 | # fk_rails_... (past_document_id => identity_documents.id) 26 | # fk_rails_... (past_identity_id => identities.id) 27 | # 28 | class Identity::Resemblance::ReusedDocumentResemblance < Identity::Resemblance 29 | belongs_to :document, class_name: "Identity::Document" 30 | belongs_to :past_document, class_name: "Identity::Document" 31 | end 32 | -------------------------------------------------------------------------------- /app/models/identity_program.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: identity_programs 4 | # 5 | # id :bigint not null, primary key 6 | # created_at :datetime not null 7 | # updated_at :datetime not null 8 | # identity_id :bigint not null 9 | # program_id :bigint not null 10 | # 11 | # Indexes 12 | # 13 | # index_identity_programs_on_identity_id (identity_id) 14 | # index_identity_programs_on_identity_id_and_program_id (identity_id,program_id) UNIQUE 15 | # index_identity_programs_on_program_id (program_id) 16 | # 17 | # Foreign Keys 18 | # 19 | # fk_rails_... (identity_id => identities.id) 20 | # fk_rails_... (program_id => programs.id) 21 | # 22 | class IdentityProgram < ApplicationRecord 23 | belongs_to :identity 24 | belongs_to :program 25 | 26 | validates :identity_id, uniqueness: { scope: :program_id } 27 | end 28 | -------------------------------------------------------------------------------- /app/models/oauth_token.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: oauth_access_tokens 4 | # 5 | # id :bigint not null, primary key 6 | # expires_in :integer 7 | # previous_refresh_token :string default(""), not null 8 | # refresh_token :string 9 | # resource_owner_type :string 10 | # revoked_at :datetime 11 | # scopes :string 12 | # token_bidx :string 13 | # token_ciphertext :text 14 | # created_at :datetime not null 15 | # application_id :bigint not null 16 | # resource_owner_id :bigint 17 | # 18 | # Indexes 19 | # 20 | # index_oauth_access_tokens_on_application_id (application_id) 21 | # index_oauth_access_tokens_on_refresh_token (refresh_token) UNIQUE 22 | # index_oauth_access_tokens_on_resource_owner_id (resource_owner_id) 23 | # index_oauth_access_tokens_on_token_bidx (token_bidx) UNIQUE 24 | # polymorphic_owner_oauth_access_tokens (resource_owner_id,resource_owner_type) 25 | # 26 | # Foreign Keys 27 | # 28 | # fk_rails_... (application_id => oauth_applications.id) 29 | # fk_rails_... (resource_owner_id => identities.id) 30 | # 31 | class OAuthToken < ApplicationRecord 32 | include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken 33 | 34 | PREFIX = "idntk." 35 | SIZE = 32 36 | 37 | scope :not_expired, -> { where(expires_in: nil).or(where("(oauth_access_tokens.created_at + make_interval(secs => expires_in)) >= ?", Time.now)) } 38 | scope :not_revoked, -> { where(revoked_at: nil).or(where(revoked_at: Time.now..)) } 39 | 40 | scope :accessible, -> { not_expired.and(not_revoked) } 41 | 42 | has_encrypted :token 43 | blind_index :token 44 | 45 | belongs_to :resource_owner, class_name: "Identity" 46 | 47 | def generate_token 48 | self.token = self.class.generate 49 | end 50 | 51 | def active? 52 | !revoked_at? && (expires_in.nil? || expires_in > 0) 53 | end 54 | 55 | def self.generate(options = {}) 56 | token_size = options.delete(:size) || SIZE 57 | PREFIX + SecureRandom.urlsafe_base64(token_size) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/policies/application_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationPolicy 4 | attr_reader :user, :record 5 | 6 | def initialize(user, record) 7 | @user = user 8 | @record = record 9 | end 10 | 11 | def index? = false 12 | 13 | def show? = false 14 | 15 | def create? = false 16 | 17 | def new? = create? 18 | 19 | def update? = false 20 | 21 | def edit? = update? 22 | 23 | def destroy? = false 24 | 25 | def user_is_manual_document_verifier? 26 | user.present? && (user.manual_document_verifier? || user.super_admin?) 27 | end 28 | 29 | class Scope 30 | def initialize(user, scope) 31 | @user = user 32 | @scope = scope 33 | end 34 | 35 | def resolve 36 | raise NoMethodError, "You must define #resolve in #{self.class}" 37 | end 38 | 39 | private 40 | 41 | attr_reader :user, :scope 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/policies/backend/user_policy.rb: -------------------------------------------------------------------------------- 1 | class Backend::UserPolicy < ApplicationPolicy 2 | def index? = user&.present? 3 | 4 | def show? = user&.present? 5 | 6 | def create? = user&.super_admin? 7 | 8 | def update? = user&.super_admin? 9 | 10 | def deactivate? = user&.super_admin? 11 | 12 | alias_method :activate?, :deactivate? 13 | end 14 | -------------------------------------------------------------------------------- /app/policies/break_glass_record_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class BreakGlassRecordPolicy < ApplicationPolicy 4 | def create? = user.present? && user.can_break_glass? 5 | end 6 | -------------------------------------------------------------------------------- /app/policies/identity/document_policy.rb: -------------------------------------------------------------------------------- 1 | class Identity::DocumentPolicy < ApplicationPolicy 2 | def index? = user_is_manual_document_verifier? 3 | 4 | def show? = user_is_manual_document_verifier? 5 | 6 | def verify? = user_is_manual_document_verifier? 7 | 8 | alias_method :approve?, :verify? 9 | alias_method :reject?, :verify? 10 | end 11 | -------------------------------------------------------------------------------- /app/policies/identity_policy.rb: -------------------------------------------------------------------------------- 1 | class IdentityPolicy < ApplicationPolicy 2 | def index? = user.present? 3 | 4 | def show? = user.present? 5 | 6 | def update? = user.present? && (user.can_break_glass? || user.super_admin?) 7 | 8 | alias_method :clear_slack_id?, :update? 9 | 10 | class Scope < ApplicationPolicy::Scope 11 | def resolve 12 | if user.super_admin? || user.manual_document_verifier? 13 | scope.all 14 | elsif user.organized_programs.any? 15 | program_ids = user.organized_programs.pluck(:id) 16 | scope.joins(:access_tokens) 17 | .where(oauth_access_tokens: { application_id: program_ids }) 18 | .distinct 19 | else 20 | scope.none 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/policies/program_policy.rb: -------------------------------------------------------------------------------- 1 | class ProgramPolicy < ApplicationPolicy 2 | def index? = user_is_program_manager? || user_has_assigned_programs? 3 | 4 | def show? = user_is_program_manager? || user_has_access_to_program? 5 | 6 | def create? = user_is_program_manager? 7 | 8 | def update? = user_is_program_manager? || user_has_access_to_program? 9 | 10 | def destroy? = user_is_program_manager? 11 | 12 | def update_basic_fields? = user_has_access_to_program? 13 | 14 | def update_scopes? = user_is_program_manager? 15 | 16 | class Scope < Scope 17 | def resolve 18 | if user.program_manager? || user.super_admin? 19 | # Program managers and super admins can see all programs 20 | scope.all 21 | else 22 | # Regular users can only see programs they are assigned to 23 | scope.joins(:organizer_positions).where(backend_organizer_positions: { backend_user_id: user.id }) 24 | end 25 | end 26 | end 27 | 28 | private 29 | 30 | def user_is_program_manager? 31 | user.present? && (user.program_manager? || user.super_admin?) 32 | end 33 | 34 | def user_has_assigned_programs? 35 | user.present? && user.organized_programs.any? 36 | end 37 | 38 | def user_has_access_to_program? 39 | user_is_program_manager? || (user.present? && user.organized_programs.include?(record)) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/policies/verification/aadhaar_verification_policy.rb: -------------------------------------------------------------------------------- 1 | class Verification::AadhaarVerificationPolicy < VerificationPolicy 2 | end 3 | -------------------------------------------------------------------------------- /app/policies/verification/document_verification_policy.rb: -------------------------------------------------------------------------------- 1 | class Verification::DocumentVerificationPolicy < VerificationPolicy 2 | end 3 | -------------------------------------------------------------------------------- /app/policies/verification/vouch_verification_policy.rb: -------------------------------------------------------------------------------- 1 | class Verification::VouchVerificationPolicy < VerificationPolicy 2 | def create? = user.super_admin? 3 | end 4 | -------------------------------------------------------------------------------- /app/policies/verification_policy.rb: -------------------------------------------------------------------------------- 1 | class VerificationPolicy < ApplicationPolicy 2 | def index? = user_is_manual_document_verifier? 3 | 4 | def pending? = user_is_manual_document_verifier? 5 | 6 | def show? = user_is_manual_document_verifier? 7 | 8 | def approve? = user_is_manual_document_verifier? 9 | 10 | def reject? = user_is_manual_document_verifier? 11 | 12 | def ignore? = user&.super_admin? 13 | end 14 | -------------------------------------------------------------------------------- /app/services/aadhaar_service.rb: -------------------------------------------------------------------------------- 1 | module AadhaarService 2 | class << self 3 | def instance 4 | @instance ||= (Rails.env.production? ? AadhaarService::Production : AadhaarService::Mock).new 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/services/aadhaar_service/mock.rb: -------------------------------------------------------------------------------- 1 | module AadhaarService 2 | class Mock 3 | def generate_step_1_link(callback_url:, redirect_url:, trans_id:) 4 | sleep Random.random_number(2..7) 5 | { 6 | status: 1, 7 | msg: "youuuuuuu", 8 | ts_trans_id: "mrow_mrrp_external_of_#{trans_id}", 9 | data: { 10 | url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 11 | } 12 | } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/services/aadhaar_service/production.rb: -------------------------------------------------------------------------------- 1 | module AadhaarService 2 | class AadhaarError < StandardError; end 3 | class FaradayErrorWithResponse < Faraday::Middleware 4 | def call(env) 5 | @app.call(env) 6 | rescue Faraday::Error => e 7 | response_body = e.response&.dig(:body) || "No response body" 8 | raise AadhaarError, "#{e.message}. Response: #{response_body}" 9 | end 10 | end 11 | 12 | class Production 13 | # this is stubbed out because exposing the implementation details of how we communicated with our aadhaar provider 14 | # opens up a route through which a malicious actor could cost us a lot of money. 15 | 16 | # if you think that's the dumbest thing you've ever heard, i'm absolutely with you. 17 | # there is a reason we stopped using them, but this fact still remains... 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/services/papers_please_engine.rb: -------------------------------------------------------------------------------- 1 | module PapersPleaseEngine 2 | def self.run(verification) 3 | tactics = case verification 4 | when Verification::DocumentVerification 5 | [] # maybe someday OCR documents & check for discrepancies? 6 | when Verification::AadhaarVerification 7 | [ AadhaarScrutinizer ] 8 | end 9 | 10 | issues = tactics.flat_map do |tactic| 11 | tactic.new(verification).run 12 | end 13 | 14 | if issues.any? 15 | verification.update(issues:) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/services/papers_please_engine/aadhaar_scrutinizer.rb: -------------------------------------------------------------------------------- 1 | module PapersPleaseEngine 2 | class AadhaarScrutinizer < Base 3 | def run 4 | identity = verification.identity 5 | identity_first_name = identity.legal_first_name.presence || identity.first_name 6 | identity_last_name = identity.legal_last_name.presence || identity.last_name 7 | identity_date_of_birth = identity.birthday 8 | identity_aadhaar_number = identity.aadhaar_number 9 | 10 | aadhaar_record = verification.aadhaar_record 11 | 12 | issues = [] 13 | 14 | split = aadhaar_record.name.split(" ") 15 | aadhaar_first_name = split.first 16 | aadhaar_last_name = split.last 17 | 18 | identity_name = "#{identity_first_name} #{identity_last_name}".downcase 19 | aadhaar_name = "#{aadhaar_first_name} #{aadhaar_last_name}".downcase 20 | 21 | if identity_name != aadhaar_name 22 | issues << if MiniLevenshtein.edit_distance(identity_name, aadhaar_name) > 4 23 | "Name doesn't seem to match" 24 | else 25 | "Name doesn't match exactly (this is probably fine)" 26 | end 27 | end 28 | 29 | if identity_date_of_birth != aadhaar_record.date_of_birth 30 | issues << "Date of birth doesn't match" 31 | end 32 | 33 | if identity_aadhaar_number[-4..] != aadhaar_record.doc_json.dig(:data, :aadhar_number)[-4..] 34 | issues << "entered Aadhaar number might not match?" 35 | end 36 | 37 | issues 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/services/papers_please_engine/base.rb: -------------------------------------------------------------------------------- 1 | module PapersPleaseEngine 2 | class Base 3 | attr_reader :verification 4 | 5 | def initialize(verification) 6 | @verification = verification 7 | end 8 | 9 | def run 10 | raise NotImplementedError, "Subclasses must implement the run method" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/services/resemblance_noticer_engine.rb: -------------------------------------------------------------------------------- 1 | module ResemblanceNoticerEngine 2 | TACTICS = [ NameSimilarity, DuplicateDocuments, EmailSubaddressing ] 3 | 4 | def self.run(identity) 5 | results = TACTICS.flat_map do |tactic| 6 | tactic.new(identity).run 7 | end 8 | 9 | results.each &:save! 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/services/resemblance_noticer_engine/base.rb: -------------------------------------------------------------------------------- 1 | module ResemblanceNoticerEngine 2 | class Base 3 | attr_reader :identity 4 | 5 | def initialize(identity) 6 | @identity = identity 7 | end 8 | 9 | def run 10 | raise NotImplementedError, "Subclasses must implement the run method" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/services/resemblance_noticer_engine/duplicate_documents.rb: -------------------------------------------------------------------------------- 1 | module ResemblanceNoticerEngine 2 | class DuplicateDocuments < Base 3 | def run 4 | checksums = identity.documents.joins(files_attachments: :blob).pluck("active_storage_blobs.checksum") 5 | return [] if checksums.empty? 6 | 7 | Identity::Document.joins(files_attachments: :blob) 8 | .joins(:identity) 9 | .where(active_storage_blobs: { checksum: checksums }) 10 | .where.not(identity: identity) 11 | .includes(:identity, files_attachments: :blob) 12 | .map do |duplicate_doc| 13 | Identity::Resemblance::ReusedDocumentResemblance.new( 14 | identity: identity, 15 | past_identity: duplicate_doc.identity, 16 | document: duplicate_doc, 17 | past_document: duplicate_doc, 18 | ) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/services/resemblance_noticer_engine/email_subaddressing.rb: -------------------------------------------------------------------------------- 1 | module ResemblanceNoticerEngine 2 | class EmailSubaddressing < Base 3 | def run 4 | return [] if identity.primary_email.blank? 5 | 6 | base_email = extract_base_email(identity.primary_email) 7 | 8 | # i'm still not convinced i understand why this SQL works... 9 | # theoretically it turns nora+1@hackclub.com and n.o.ra@hackclub.com into nora@hackclub.com? 10 | normalized_email_sql = <<~SQL.squish 11 | CONCAT( 12 | REPLACE(SPLIT_PART(SPLIT_PART(primary_email, '@', 1), '+', 1), '.', ''), 13 | '@', 14 | SPLIT_PART(primary_email, '@', 2) 15 | ) 16 | SQL 17 | 18 | similar_identities = Identity.where.not(id: identity.id) 19 | .where("#{normalized_email_sql} = ?", base_email) 20 | .where.not(primary_email: identity.primary_email) # not this one lol! 21 | 22 | similar_identities.map do |similar_identity| 23 | other_base_email = extract_base_email(similar_identity.primary_email) 24 | next unless other_base_email == base_email 25 | 26 | Identity::Resemblance::EmailSubaddressResemblance.new( 27 | identity: identity, 28 | past_identity: similar_identity, 29 | ) 30 | end.compact 31 | end 32 | 33 | private 34 | 35 | def extract_base_email(email) 36 | local_part, domain = email.split("@", 2) 37 | 38 | base_local_part = local_part.split("+").first 39 | base_local_part = base_local_part.gsub(".", "") 40 | 41 | "#{base_local_part}@#{domain}" 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/services/resemblance_noticer_engine/name_similarity.rb: -------------------------------------------------------------------------------- 1 | module ResemblanceNoticerEngine 2 | class NameSimilarity < Base 3 | def run 4 | # for now, just exact matches. 5 | # TODO: levenshtein or smth in the future? 6 | 7 | # Check all combinations of first_name/legal_first_name and last_name/legal_last_name 8 | query = Identity.none 9 | 10 | # Collect all possible first name and last name values from the identity (case insensitive) 11 | first_names = [ identity.first_name, identity.legal_first_name ].compact_blank.map(&:downcase).uniq 12 | last_names = [ identity.last_name, identity.legal_last_name ].compact_blank.map(&:downcase).uniq 13 | 14 | # i feel like this could be better... 15 | first_names.each do |fname| 16 | last_names.each do |lname| 17 | query = query.or( 18 | Identity.where("LOWER(first_name) = ? AND LOWER(last_name) = ?", fname, lname) 19 | .or(Identity.where("LOWER(legal_first_name) = ? AND LOWER(legal_last_name) = ?", fname, lname)) 20 | .or(Identity.where("LOWER(first_name) = ? AND LOWER(legal_last_name) = ?", fname, lname)) 21 | .or(Identity.where("LOWER(legal_first_name) = ? AND LOWER(last_name) = ?", fname, lname)) 22 | ) 23 | end 24 | end 25 | 26 | query.where.not(id: identity.id).map do |similar_identity| 27 | Identity::Resemblance::NameResemblance.new( 28 | identity: identity, 29 | past_identity: similar_identity, 30 | ) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/services/slack_service.rb: -------------------------------------------------------------------------------- 1 | module SlackService 2 | class << self 3 | def client = @client ||= Slack::Web::Client.new 4 | 5 | def find_by_email(email) = client.users_lookupByEmail(email:).dig("user", "id") rescue nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/aadhaar/digilocker_link.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to "continue!", digilocker_redirect_aadhaar_path, target: "_blank", role: "button" %> 2 | -------------------------------------------------------------------------------- /app/views/addresses/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with model: address, url: local_assigns[:url], local: true do |f| %> 2 | <% if local_assigns[:from_program] %> 3 | <%= f.hidden_field :from_program, value: true %> 4 | <% end %> 5 | <% if address.errors.any? %> 6 | 14 | <% end %> 15 | 16 |
17 | Address Details 18 |
19 | <%= f.text_field :first_name, placeholder: "First name", required: true %> 20 | <%= f.text_field :last_name, placeholder: "Last name", required: true %> 21 |
22 | <%= f.text_field :line_1, placeholder: "Address line 1", required: true %> 23 | <%= f.text_field :line_2, placeholder: "Address line 2 (optional)" %> 24 |
25 | <%= f.text_field :city, placeholder: "City", required: true %> 26 | <%= f.text_field :state, placeholder: "State/Province", required: true %> 27 |
28 | <%= f.text_field :postal_code, placeholder: "Postal code", required: true %> 29 | <%= f.label :country, "Country" %> 30 | <%= f.collection_select :country, Address.countries_for_select, 31 | :first, :last, 32 | { include_blank: "Select a country" }, 33 | { required: true } %> 34 |
35 | 36 |
37 | <%= f.submit local_assigns[:submit] || "Save Address" %> 38 |
39 | <% end %> 40 | -------------------------------------------------------------------------------- /app/views/addresses/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit Address

2 | 3 | <%= render 'form', address: @address %> 4 | 5 | <%= link_to "Show", @address %> | 6 | <%= link_to "Back", addresses_path %> 7 | -------------------------------------------------------------------------------- /app/views/addresses/index.html.erb: -------------------------------------------------------------------------------- 1 |

your addresses

2 | <%= render Components::HomeButton.new %>
3 |
4 | <%= link_to "add a new address", new_address_path, class: "primary" %> 5 | <% if @addresses.any? %> 6 |
7 | <% @addresses.each do |address| %> 8 |
9 |
10 |

<%= address.first_name %> <%= address.last_name %> 11 | <% if current_identity.primary_address == address %> 12 | 🏠 13 | <% end %> 14 |

15 |

16 | <%= address.line_1 %>
17 | <% if address.line_2.present? %> 18 | <%= address.line_2 %>
19 | <% end %> 20 | <%= address.city %>, <%= address.state %> <%= address.postal_code %>
21 | <%= address.country %> 22 |

23 |
24 |
25 | <%= button_to "edit", edit_address_path(address), method: :get, style: "display: inline;" %> 26 | <%= button_to "delete", address, method: :delete, 27 | data: { confirm: "Are you sure?" }, style: "display: inline;" %> 28 | <% unless current_identity.primary_address == address %> 29 | <%= button_to "make primary", address_path(address, make_primary: true), 30 | method: :patch, style: "display: inline;" %> 31 | <% end %> 32 |
33 |
34 | <% end %> 35 |
36 | <% else %> 37 |

You haven't added any addresses yet.

38 | <% end %> 39 | -------------------------------------------------------------------------------- /app/views/addresses/new.html.erb: -------------------------------------------------------------------------------- 1 |

add your address!

2 | <%= render 'form', address: @address %> 3 | <%= link_to "Back", addresses_path %> 4 | -------------------------------------------------------------------------------- /app/views/addresses/program_create_address.html.erb: -------------------------------------------------------------------------------- 1 |

we'll need your address...

2 | (please use a real address, we're going to send you something here!)

3 | <%= render 'form', address: @address, submit: "continue", from_program: true %> 4 | -------------------------------------------------------------------------------- /app/views/addresses/show.html.erb: -------------------------------------------------------------------------------- 1 |

Addresses#show

2 |

Find me in app/views/addresses/show.html.erb

3 | -------------------------------------------------------------------------------- /app/views/api/v1/addresses/_address.jb: -------------------------------------------------------------------------------- 1 | { 2 | id: address.public_id, 3 | first_name: address.first_name, 4 | last_name: address.last_name, 5 | line_1: address.line_1, 6 | line_2: address.line_2, 7 | city: address.city, 8 | state: address.state, 9 | postal_code: address.postal_code, 10 | country: address.country, 11 | primary: address.id == address&.identity&.primary_address&.id 12 | }.compact_blank 13 | -------------------------------------------------------------------------------- /app/views/api/v1/identities/_identity.jb: -------------------------------------------------------------------------------- 1 | ident = { 2 | id: identity.public_id, 3 | ysws_eligible: identity.ysws_eligible, 4 | verification_status: identity.verification_status, 5 | verification_status_reason: identity.verification_status_reason, 6 | rejection_reason: identity.verification_status_reason, 7 | rejection_reason_details: identity.verification_status_reason_details, 8 | birthday: identity.birthday 9 | } 10 | 11 | scope "basic_info" do 12 | ident[:first_name] = identity.first_name 13 | ident[:last_name] = identity.last_name 14 | ident[:primary_email] = identity.primary_email 15 | ident[:slack_id] = identity.slack_id 16 | ident[:primary_email] = identity.primary_email 17 | ident[:phone_number] = identity.phone_number 18 | end 19 | 20 | scope "legal_name" do 21 | ident[:legal_first_name] = identity.legal_first_name 22 | ident[:legal_last_name] = identity.legal_last_name 23 | end 24 | 25 | scope "address" do 26 | ident[:addresses] = identity.addresses.map do |address| 27 | render address 28 | end 29 | end 30 | 31 | ident.compact_blank 32 | -------------------------------------------------------------------------------- /app/views/api/v1/identities/index.jb: -------------------------------------------------------------------------------- 1 | { 2 | identities: @identities.map { |identity| render(identity) } 3 | } 4 | -------------------------------------------------------------------------------- /app/views/api/v1/identities/me.jb: -------------------------------------------------------------------------------- 1 | { 2 | identity: render(@identity), 3 | scopes: current_scopes 4 | } 5 | -------------------------------------------------------------------------------- /app/views/api/v1/identities/show.jb: -------------------------------------------------------------------------------- 1 | { 2 | identity: render(@identity) 3 | } 4 | -------------------------------------------------------------------------------- /app/views/backend/audit_logs/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("Audit Logs", close_url: backend_root_path, max_width: 1000) do %> 2 | Filter: 3 |
4 | <% if params[:admin_actions_only] %> 5 | <%= link_to "Show all actions", backend_audit_logs_path, method: :get, class: "button" %> 6 | <% else %> 7 | <%= link_to "Show only admin actions", backend_audit_logs_path(admin_actions_only: true), class: "button" %> 8 | <% end %> 9 |
10 | <%= render Components::PublicActivity::Container.new(@activities) %> 11 | <%= paginate @activities %> 12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/backend/identities/_identity.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::UserMention.new(identity) %> 2 | -------------------------------------------------------------------------------- /app/views/backend/identities/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("Identities", close_url: backend_root_path, max_width: 1000) do %> 2 |
3 | 4 | <%= form_with url: backend_identities_path, method: :get, local: true, class: "search-form" do |f| %> 5 |
6 |
7 | <%= f.search_field :search, value: params[:search], placeholder: "Search identities...", class: "form-control" %> 8 |
9 |
10 | <%= f.submit "Search", class: "btn btn-primary" %> 11 | <% if params[:search].present? %> 12 | <%= link_to "Clear", backend_identities_path, class: "btn btn-secondary" %> 13 | <% end %> 14 |
15 |
16 | <% end %> 17 | 18 | <% if params[:search].present? %> 19 |

20 | Found <%= pluralize(@identities.total_count, 'identity') %> matching "<%= params[:search] %>" 21 |

22 | <% end %> 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | <% @identities.each do |identity| %> 35 | 36 | 37 | 38 | 39 | 40 | 41 | <% end %> 42 | 43 |
NameEmailVerification StatusYSWS Eligible?
<%= render identity %><%= identity.primary_email %><%= identity.verification_status %><%= identity.ysws_eligible.nil? ? "?" : render_checkbox(identity.ysws_eligible) %>
44 | 45 | 46 |
47 | <%= paginate @identities, params: { search: params[:search] } %> 48 |
49 |
50 | <% end %> 51 | -------------------------------------------------------------------------------- /app/views/backend/identities/new_vouch.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("Create Vouch Verification for #{@identity.first_name} #{@identity.last_name}", close_url: backend_identity_path(@identity)) do %> 2 |
3 | <%= form_with model: [@identity, @vouch], url: create_vouch_backend_identity_path(@identity), method: :post, local: true do |f| %> 4 |
5 | <%= f.label :evidence, "Upload Evidence Picture" %> 6 | <%= f.file_field :evidence, required: true %> 7 |
8 | <%= f.submit "Create Vouch" %> 9 | <% end %> 10 |
11 | <% end %> 12 | -------------------------------------------------------------------------------- /app/views/backend/identity/resemblance/email_subaddress_resemblances/_email_subaddress_resemblance.html.erb: -------------------------------------------------------------------------------- 1 | This email address may be a subaddressed version of an existing identity's email address: 2 |
    3 |
  • This identity: <%= email_subaddress_resemblance.identity.primary_email %>
  • 4 |
  • Previous identity: <%= email_subaddress_resemblance.past_identity.primary_email %>
  • 5 |
6 | -------------------------------------------------------------------------------- /app/views/backend/identity/resemblance/name_resemblances/_name_resemblance.html.erb: -------------------------------------------------------------------------------- 1 | These identities have similar names: 2 | -------------------------------------------------------------------------------- /app/views/backend/identity/resemblance/reused_document_resemblances/_reused_document_resemblance.html.erb: -------------------------------------------------------------------------------- 1 | This exact document has already been used for a different identity <%= link_to "here", backend_verification_path(reused_document_resemblance.document.verification), target: "_blank" %>: 2 | -------------------------------------------------------------------------------- /app/views/backend/programs/_program.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 🥐 3 | <%= link_to backend_program_path(program), class: "identity-link", target: "_blank" do %> 4 | <%= program.name %> 5 | <% end %> 6 | 7 | -------------------------------------------------------------------------------- /app/views/backend/programs/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("Edit Program: #{@program.name}", close_url: backend_program_path(@program), max_width: 500) do %> 2 |
3 | <%= render Backend::Programs::Form.new @program %> 4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/backend/programs/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("Programs", close_url: backend_root_path, max_width: 800) do %> 2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% @programs.each do |program| %> 17 | 18 | 27 | 33 | 40 | 41 | 42 | 45 | 46 | <% end %> 47 | 48 |
Program NameOAuth ApplicationScopesUsersActive?Actions
19 |
20 | <%= program.name %> 21 | <% if program.description.present? %> 22 |
23 | <%= truncate(program.description, length: 60) %> 24 | <% end %> 25 |
26 |
28 | 29 | ID: <%= program.uid %>
30 | Name: <%= program.name %> 31 |
32 |
34 | <% if program.scopes.present? %> 35 | <%= program.scopes %> 36 | <% else %> 37 | No scopes 38 | <% end %> 39 | <%= program.identities.distinct.count %><%= render_checkbox(program.active?) %> 43 | <%= link_to "View", backend_program_path(program), class: "link" %> 44 |
49 |
50 | <%= link_to "New Program", new_backend_program_path, class: "button" %> 51 |
52 |
53 |
54 | <% end %> 55 | -------------------------------------------------------------------------------- /app/views/backend/programs/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("New Program", close_url: backend_programs_path, max_width: 500) do %> 2 |
3 | <%= render Backend::Programs::Form.new @program %> 4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/backend/static_pages/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("Identity Vault (#{Rails.env.upcase})") do %> 2 |
3 | Would you like to... 4 |
    5 | <% super_admin_tool do %> 6 |
  • 7 | <%= link_to "manage users", backend_users_path, { class: 'link' } %>? 8 |
  • 9 |
  • 10 | do a <%= link_to "good job", backend_good_job_path, { class: 'link' } %> today? 11 |
  • 12 |
  • 13 | <%= link_to "audit", backend_audits1984_path, { class: 'link' } %> console sessions? 14 |
  • 15 |
  • 16 | flip some <%= link_to "feature flags", backend_flipper_path, { class: 'link' } %>? 17 |
  • 18 | <% end %> 19 | <% program_manager_tool do %> 20 |
  • 21 | <%= link_to "manage programs", backend_programs_path, { class: 'link' } %>? 22 |
  • 23 | <% end %> 24 | <% mdv_tool do %> 25 |
  • 26 | <%= link_to "review pending verifications", pending_backend_verifications_path, { class: 'link' } %> 27 | <% if @pending_verifications_count && @pending_verifications_count > 0 %> 28 | (<%= @pending_verifications_count %> pending) 29 | <% end %>? 30 |
  • 31 | <% end %> 32 |
  • 33 | view <%= link_to "audit logs", backend_audit_logs_path, { class: 'link' } %>? 34 |
  • 35 |
  • 36 | view <%= link_to "identities", backend_identities_path, { class: 'link' } %>? 37 |
  • 38 |
  • 39 | view <%= link_to "dashboard", backend_dashboard_path, { class: 'link' } %>? 40 |
  • 41 |
42 |
43 | <% end %> 44 | -------------------------------------------------------------------------------- /app/views/backend/static_pages/login.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Identity Vault [ 5 | <% case Rails.env %> 6 | <% when "development" %> 7 | DEV 8 | <% when "staging" %> 9 | STAGING 10 | <% else %> 11 | PROD 12 | <% end %>] 13 |
14 |
15 |
16 |
17 | Abandon hope, all ye who enter here... 18 |
19 |
20 | <%= link_to backend_slack_auth_path, class: "button w-fit" do %> 21 | Sign in with Slack? 22 | <% end %> 23 | <% dev_tool do %> 24 | <%= form_with url: backend_fake_slack_callback_for_dev_path, method: :post do |f| %> 25 | <%= f.text_field :slack_id %> 26 | <%= f.submit "fake it til' you make it" %> 27 | <% end %> 28 | <% end %> 29 |
30 |
31 | Running commit <%= ENV["SOURCE_COMMIT"]&.[](0..7) || "...dunno?" %> 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /app/views/backend/static_pages/session_dump.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("Session dump", close_url: backend_root_path) do %> 2 | <%== ap session %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/backend/users/_user.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::UserMention.new(user) %> 2 | -------------------------------------------------------------------------------- /app/views/backend/users/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("Edit user: #{@user.username}", close_url: backend_users_path, max_width: 500) do %> 2 |
3 | <%= render Backend::Users::Form.new @user %> 4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/backend/users/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("Users", close_url: backend_root_path, max_width: 600) do %> 2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% @users.each do |user| %> 15 | 16 | 19 | 20 | 21 | 22 | 23 | <% end %> 24 | 25 |
UserRolesActive?View
17 | <%= render user %> 18 | <%= user.pretty_roles %><%= render_checkbox(user.active?) %><%= link_to "go!", user, class: "link" %>
26 |
27 |
28 |
29 | <%= link_to new_backend_user_path, class: "button w-fit" do %> 30 | + create user 31 | <% end %> 32 |
33 | <% end %> 34 | -------------------------------------------------------------------------------- /app/views/backend/users/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("New User", close_url: backend_users_path, max_width: 500) do %> 2 |
3 | <%= render Backend::Users::Form.new @user %> 4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/backend/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("User: #{@user.username}", close_url: backend_users_path) do %> 2 |
3 | <%= render Components::UserMention.new(@user) %> 4 | Roles: <%= @user.pretty_roles %> 5 |
6 | Organized Programs: 7 | <% if @user.organized_programs.any? %> 8 | <%= @user.organized_programs.map(&:name).join(", ") %> 9 | <% else %> 10 | None 11 | <% end %> 12 | <% super_admin_tool do %> 13 | <%= link_to "edit this user", edit_backend_user_path(@user), class: "link" %> 14 | <% if @user.active? %> 15 | <%= button_to "deactivate this user", {action: :deactivate} %> 16 | (this will stop them from logging in) 17 | <% else %> 18 | <%= button_to "activate this user", {action: :activate} %> 19 | (this will allow them to log in again) 20 | <% end %> 21 | <% end %> 22 |
23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Views::Base < Components::Base 4 | # The `Views::Base` is an abstract class for all your views. 5 | 6 | # By default, it inherits from `Components::Base`, but you 7 | # can change that to `Phlex::HTML` if you want to keep views and 8 | # components independent. 9 | end 10 | -------------------------------------------------------------------------------- /app/views/doorkeeper/applications/_delete_form.html.erb: -------------------------------------------------------------------------------- 1 | <%- submit_btn_css ||= 'btn btn-link' %> 2 | <%= form_tag oauth_application_path(application), method: :delete do %> 3 | <%= submit_tag t('doorkeeper.applications.buttons.destroy'), 4 | onclick: "return confirm('#{ t('doorkeeper.applications.confirmations.destroy') }')", 5 | class: submit_btn_css %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/doorkeeper/applications/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

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

3 |
4 | 5 | <%= render 'form', application: @application %> 6 | -------------------------------------------------------------------------------- /app/views/doorkeeper/applications/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

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

3 |
4 |

<%= link_to t('.new'), new_oauth_application_path, class: 'btn btn-success' %>

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% @applications.each do |application| %> 17 | 18 | 21 | 24 | 27 | 30 | 31 | <% end %> 32 | 33 |
<%= t('.name') %><%= t('.callback_url') %><%= t('.confidential') %><%= t('.actions') %>
19 | <%= link_to application.name, oauth_application_path(application) %> 20 | 22 | <%= simple_format(application.redirect_uri) %> 23 | 25 | <%= application.confidential? ? t('doorkeeper.applications.index.confidentiality.yes') : t('doorkeeper.applications.index.confidentiality.no') %> 26 | 28 | <%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %> 29 |
34 | -------------------------------------------------------------------------------- /app/views/doorkeeper/applications/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

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

3 |
4 | 5 | <%= render 'form', application: @application %> 6 | -------------------------------------------------------------------------------- /app/views/doorkeeper/applications/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= t('.title', name: @application.name) %>

3 |
4 |
5 |
6 |

<%= t('.application_id') %>:

7 |

<%= @application.uid %>

8 |

<%= t('.secret') %>:

9 |

10 | 11 | <% secret = flash[:application_secret].presence || @application.plaintext_secret %> 12 | <% if secret.blank? && Doorkeeper.config.application_secret_hashed? %> 13 | <%= t('.secret_hashed') %> 14 | <% else %> 15 | <%= secret %> 16 | <% end %> 17 | 18 |

19 |

<%= t('.scopes') %>:

20 |

21 | 22 | <% if @application.scopes.present? %> 23 | <%= @application.scopes %> 24 | <% else %> 25 | <%= t('.not_defined') %> 26 | <% end %> 27 | 28 |

29 |

<%= t('.confidential') %>:

30 |

<%= @application.confidential? %>

31 |

<%= t('.callback_urls') %>:

32 | <% if @application.redirect_uri.present? %> 33 | 34 | <% @application.redirect_uri.split.each do |uri| %> 35 | 36 | 39 | 42 | 43 | <% end %> 44 |
37 | <%= uri %> 38 | 40 | <%= link_to t('doorkeeper.applications.buttons.authorize'), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code', scope: @application.scopes), class: 'btn btn-success', target: '_blank' %> 41 |
45 | <% else %> 46 | <%= t('.not_defined') %> 47 | <% end %> 48 |
49 |
50 |

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

51 |

<%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(@application), class: 'btn btn-primary' %>

52 |
53 |
54 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorizations/error.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Something isn't right about this OAuth request...

3 |
4 |
5 |
6 |     <%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %>
7 |   
8 |
9 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorizations/form_post.html.erb: -------------------------------------------------------------------------------- 1 | 4 | 5 | <%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false do %> 6 | <% auth.body.compact.each do |key, value| %> 7 | <%= hidden_field_tag key, value %> 8 | <% end %> 9 | <% end %> 10 | 11 | 16 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorizations/show.html.erb: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | <%= params[:code] %> 7 |
8 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorized_applications/_delete_form.html.erb: -------------------------------------------------------------------------------- 1 | <%- submit_btn_css ||= 'btn btn-link' %> 2 | <%= form_tag oauth_authorized_application_path(application), method: :delete do %> 3 | <%= submit_tag t('doorkeeper.authorized_applications.buttons.revoke'), onclick: "return confirm('#{ t('doorkeeper.authorized_applications.confirmations.revoke') }')", class: submit_btn_css %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorized_applications/index.html.erb: -------------------------------------------------------------------------------- 1 | 4 | <%= render Components::HomeButton.new %> 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% @applications.each do |application| %> 17 | 18 | 19 | 20 | 21 | <% end %> 22 | 23 |
ProgramGranted access at
<%= application.name %><%= application.created_at.strftime(t('doorkeeper.authorized_applications.index.date_format')) %>
24 |
25 | -------------------------------------------------------------------------------- /app/views/forms/application_form.rb: -------------------------------------------------------------------------------- 1 | class ApplicationForm < Superform::Rails::Form 2 | include Phlex::Rails::Helpers::Pluralize 3 | include Phlex::Rails::Helpers::CheckBoxTag 4 | register_value_helper :program_manager_tool 5 | register_value_helper :super_admin_tool 6 | register_value_helper :mdv_tool 7 | register_value_helper :dev_tool 8 | 9 | def labeled(component, label) 10 | render label(class: "grid-input-1", for: component.dom.id) { label } 11 | span(class: "grid-input-2") { render component } 12 | end 13 | 14 | def check_box(field, description = nil) 15 | div class: "flex-column" do 16 | div class: "checkbox-row" do 17 | render field.checkbox 18 | render field.label 19 | end 20 | i { safe(description) } if description 21 | end 22 | end 23 | 24 | def row(component) 25 | div do 26 | render component.field.label(style: "display: block;") 27 | render component 28 | end 29 | end 30 | 31 | def around_template(&) 32 | super do 33 | error_messages 34 | yield 35 | end 36 | end 37 | 38 | def error_messages 39 | if model.errors.any? 40 | div(style: "color: red;") do 41 | h2 { "#{pluralize model.errors.count, "error"} prohibited this post from being saved:" } 42 | ul do 43 | model.errors.each do |error| 44 | li { error.full_message } 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/views/forms/backend/programs/form.rb: -------------------------------------------------------------------------------- 1 | class Backend::Programs::Form < ApplicationForm 2 | def view_template(&) 3 | div do 4 | labeled field(:name).input, "Program Name: " 5 | end 6 | div do 7 | label(class: "field-label") { "Redirect URIs (one per line):" } 8 | textarea( 9 | name: "oauth_application[redirect_uri]", 10 | placeholder: "https://example.com/callback", 11 | class: "input-field", 12 | rows: 3, 13 | style: "width: 100%;", 14 | ) { model.redirect_uri } 15 | end 16 | program_manager_tool do 17 | div style: "margin: 1rem 0;" do 18 | label(class: "field-label") { "OAuth Scopes:" } 19 | # Hidden field to ensure empty scopes array is submitted when no checkboxes are checked 20 | input type: "hidden", name: "program[scopes_array][]", value: "" 21 | Program::AVAILABLE_SCOPES.each do |scope| 22 | div class: "checkbox-row" do 23 | scope_checked = model.persisted? ? model.has_scope?(scope[:name]) : false 24 | input( 25 | type: "checkbox", 26 | name: "program[scopes_array][]", 27 | value: scope[:name], 28 | id: "program_scopes_#{scope[:name]}", 29 | checked: scope_checked, 30 | ) 31 | label(for: "program_scopes_#{scope[:name]}", class: "checkbox-label", style: "margin-right: 0.5rem;") { scope[:name] } 32 | small { scope[:description] } 33 | end 34 | end 35 | end 36 | end 37 | 38 | submit model.new_record? ? "Create Program" : "Update Program" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/views/forms/backend/users/form.rb: -------------------------------------------------------------------------------- 1 | # params.require(:backend_user).permit(:slack_id, :username, :all_fields_access, :human_endorser, :program_manager, :manual_document_verifier, :super_admin) 2 | 3 | class Backend::Users::Form < ApplicationForm 4 | def view_template(&) 5 | div do 6 | labeled field(:slack_id).input(disabled: !model.new_record?), "Slack ID: " 7 | end 8 | div do 9 | labeled field(:username).input, "Display Name: " 10 | end 11 | b { "Roles: " } 12 | div class: "grid gap align-center", style: "grid-template-columns: max-content auto;" do 13 | check_box(field(:super_admin), "Allows this user access to all permissions
(this includes managing other users)") 14 | check_box(field(:program_manager), "This user can provision API keys and program tags.") 15 | check_box(field(:human_endorser), "This user can mark identities as
human-endorsed.") 16 | check_box(field(:all_fields_access), "This user can view all fields on all identities.") 17 | check_box(field(:manual_document_verifier), "This user can mark documents as
manually verified.") 18 | check_box(field(:can_break_glass), "This user can view ID docs after they've been reviewed.") 19 | end 20 | 21 | b { "Program Organizer Positions: " } 22 | div class: "grid gap", style: "grid-template-columns: 1fr;" do 23 | Program.all.each do |program| 24 | is_organizer = model.organized_programs.include?(program) 25 | 26 | div class: "flex-column" do 27 | div class: "checkbox-row" do 28 | check_box_tag("backend_user[organized_program_ids][]", program.id, is_organizer, id: "organized_program_#{program.id}") 29 | label(for: "organized_program_#{program.id}") { program.name } 30 | end 31 | end 32 | end 33 | end 34 | 35 | submit model.new_record? ? "create!" : "save" 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/views/kaminari/_first_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "First" page 2 | - available local variables 3 | url: url to the first page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote -%> 8 | <%= link_to "First", url, {remote: remote, class: "button button-secondary"} %> 9 | -------------------------------------------------------------------------------- /app/views/kaminari/_gap.html.erb: -------------------------------------------------------------------------------- 1 | <%# Non-link tag that stands for skipped pages... 2 | - available local variables 3 | current_page: a page object for the currently displayed page 4 | total_pages: total number of pages 5 | per_page: number of items to fetch per page 6 | remote: data-remote -%> 7 | 8 | -------------------------------------------------------------------------------- /app/views/kaminari/_last_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "Last" page 2 | - available local variables 3 | url: url to the last page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote -%> 8 | <%= link_to "Last", url, {remote: remote, class: "button button-secondary"} %> 9 | -------------------------------------------------------------------------------- /app/views/kaminari/_next_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "Next" page 2 | - available local variables 3 | url: url to the next page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote -%> 8 | <%= link_to "Next →", url, {rel: 'next', remote: remote, class: "button"} %> 9 | -------------------------------------------------------------------------------- /app/views/kaminari/_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link showing page number 2 | - available local variables 3 | page: a page object for "this" page 4 | url: url to this page 5 | current_page: a page object for the currently displayed page 6 | total_pages: total number of pages 7 | per_page: number of items to fetch per page 8 | remote: data-remote -%> 9 | <% if page.current? %> 10 | <%= page %> 11 | <% else %> 12 | <%= link_to page, url, {remote: remote, rel: page.rel, class: "button", style: "min-width: 2rem;"} %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/kaminari/_paginator.html.erb: -------------------------------------------------------------------------------- 1 | <%# The container tag 2 | - available local variables 3 | current_page: a page object for the currently displayed page 4 | total_pages: total number of pages 5 | per_page: number of items to fetch per page 6 | remote: data-remote 7 | paginator: the paginator that renders the pagination tags inside -%> 8 | <%= paginator.render do -%> 9 | 24 | <% end -%> 25 | -------------------------------------------------------------------------------- /app/views/kaminari/_prev_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "Previous" page 2 | - available local variables 3 | url: url to the previous page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote -%> 8 | <%= link_to "← Prev", url, {rel: 'prev', remote: remote, class: "button"} %> 9 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= content_for(:title) || "Identity Vault" %> 5 | 6 | 7 | 8 | <%= csrf_meta_tags %> 9 | <%= csp_meta_tag %> 10 | <%= yield :head %> 11 | 17 | 18 | 19 | 20 | <%= vite_client_tag %> 21 | <%= vite_stylesheet_tag "application.css" %> 22 | <%= vite_javascript_tag 'application' %> 23 | 24 | 25 |
26 | <%= render Components::Brand.new(identity: current_identity) %> 27 | <%= render "shared/flash" %> 28 | 29 | <%= yield %> 30 | <%= render Components::Footer.new %> 31 |
32 | <% unless Rails.env.production? %> 33 |
34 | <% end %> 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/views/layouts/backend.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= content_for(:title) || "Identity Vault" %> 5 | 6 | 7 | 8 | <%= csrf_meta_tags %> 9 | <%= csp_meta_tag %> 10 | <%= yield :head %> 11 | <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> 12 | <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> 13 | 14 | 15 | 16 | <%# Includes all stylesheet files in app/assets/stylesheets %> 17 | <%= vite_client_tag %> 18 | <%= vite_stylesheet_tag "backend.css" %> 19 | <%= vite_javascript_tag 'backend' %> 20 | 21 | 22 | <%= render "shared/flash" %> 23 | <%= yield %> 24 | <% unless Rails.env.production? %> 25 |
26 | <% end %> 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/views/layouts/doorkeeper/admin.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= t('doorkeeper.layouts.admin.title') %> 8 | <%= stylesheet_link_tag "doorkeeper/admin/application" %> 9 | <%= csrf_meta_tags %> 10 | 11 | 12 | 28 | 29 |
30 | <%- if flash[:notice].present? %> 31 |
32 | <%= flash[:notice] %> 33 |
34 | <% end -%> 35 | 36 | <%= yield %> 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= { 2 | transactionalId: @TRANSACTIONAL_ID, 3 | email: @recipient, 4 | dataVariables: @datavariables.merge({ 5 | env: case Rails.env 6 | when "development" 7 | "[DEV] " 8 | when "staging" 9 | "[STAGING] " 10 | else 11 | "​" 12 | end, 13 | }) 14 | }.to_json %> -------------------------------------------------------------------------------- /app/views/mailers/blank_mailer.text.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/app/views/mailers/blank_mailer.text.erb -------------------------------------------------------------------------------- /app/views/onboardings/aadhaar.html.erb: -------------------------------------------------------------------------------- 1 |

Aadhaar Number Verification

2 |

As an Indian resident, we will verify your identity using your Aadhaar number.

3 | <% if @identity.errors.any? %> 4 |
5 |

<%= pluralize(@identity.errors.count, "error") %> prohibited this from being saved:

6 |
    7 | <% @identity.errors.full_messages.each do |message| %> 8 |
  • <%= message %>
  • 9 | <% end %> 10 |
11 |
12 | <% end %> 13 | <%= form_with model: @identity, url: aadhaar_onboarding_path, method: :post, local: true do |form| %> 14 |
15 | <%= form.label :aadhaar_number, "Enter your 12-digit Aadhaar number:" %> 16 | <%= form.text_field :aadhaar_number, 17 | value: @identity.aadhaar_number, 18 | placeholder: "123456789012", 19 | required: true, 20 | pattern: "\\d{12}", 21 | maxlength: "12", 22 | style: "font-family: monospace; font-size: 1.2em; letter-spacing: 2px;" %> 23 | Your Aadhaar number is encrypted and stored securely. We use it only for identity verification. 24 |
25 |
26 | <%= form.submit "Verify Aadhaar →" %> 27 |
28 | <% end %> 29 | 38 | -------------------------------------------------------------------------------- /app/views/onboardings/aadhaar_step_2.html.erb: -------------------------------------------------------------------------------- 1 |

Continue with Digilocker

2 | <%= render Components::BootlegTurbo.new(async_digilocker_link_aadhaar_path, text: "generating link...") %> 3 | -------------------------------------------------------------------------------- /app/views/onboardings/address.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Mailing Address

4 | <% case session.dig(:stashed_data, "context") %> 5 | <% when "stickers" %> 6 |

We need your mailing address to send you stickers.

7 | <% else %> 8 |

Please provide your current mailing address.

9 | <% end %> 10 |
11 | 12 | <%= render 'addresses/form', address: @address, url: address_onboarding_path, submit: "Continue →" %> 13 |
14 | -------------------------------------------------------------------------------- /app/views/onboardings/submitted.html.erb: -------------------------------------------------------------------------------- 1 | <%= render "shared/verification_status", identity: @identity %> 2 |
3 | <% if session[:oauth_return_to] %> 4 | <%= link_to "Continue →", continue_onboarding_path, role: "button" %> 5 | <% else %> 6 | <%= render Components::HomeButton.new %> 7 | <% end %> 8 |
9 | -------------------------------------------------------------------------------- /app/views/onboardings/welcome.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome to Hack Club's identity platform!

2 |

3 | <% case session.dig(:stashed_data, "context") %> 4 | <% when "stickers" %> 5 | We are excited to send you free stickers 6 | <% else %> 7 | We are excited to meet you 8 | <% end %> 9 | – but first, we need to verify that you are 18 years old or under.

10 |

In the past year, Hack Club has given out ~$1M in grants to students like you, and with that comes a lot of adults trying to slip in.

11 |
12 |
13 |
14 |

✨ I'm new here!

15 |

Let's get you set up with a new account

16 | <%= link_to "Get started →", basic_info_onboarding_path, 17 | role: "button" %> 18 |
19 |
20 |

🔑 I've been here before...

21 |

Sign in to continue where you left off

22 | <%= link_to "Sign in →", signin_onboarding_path, 23 | role: "button", 24 | class: "secondary" %> 25 |
26 |
27 | -------------------------------------------------------------------------------- /app/views/public_activity/break_glass_record/_create.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | <% if activity.trackable&.automatic? %> 3 | automatically 4 | <% end %> 5 | broke the glass on <%= render activity.trackable&.break_glassable&.identity if activity.trackable&.break_glassable&.identity %>'s <%= link_to "verification", backend_verification_path(activity.trackable.break_glassable.verification) if activity.trackable.present? %> 6 | <%= activity.trackable&.reason %>. 7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/public_activity/identity/_admin_update.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | edited <%= render activity.trackable %>'s identity. (<%= activity.parameters[:reason] %>) 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/identity/_clear_slack_id.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | cleared <%= render activity.trackable %>'s linked Slack ID 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/identity/_create.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity, owner: activity.trackable) do %> 2 | created an identity. 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/identity/_set_slack_id.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | set this identity's Slack ID. 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/identity/_update.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | edited <%= render activity.trackable %>'s identity. 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/verification/_approve.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | approved <%= render activity.trackable.identity %>'s <%= link_to "verification", backend_verification_path(activity.trackable) %> 3 | <% unless activity.parameters[:ysws_eligible].nil? %> 4 | as YSWS <%= activity.parameters[:ysws_eligible] ? "eligible" : "ineligible" %>. 5 | <% end %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/public_activity/verification/_create.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | started the verification process. 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/verification/_reject.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | rejected <%= render activity.trackable.identity %>'s <%= link_to "verification", backend_verification_path(activity.trackable) %> 3 | <% unless activity.parameters[:reason].nil? %> 4 | for <%= Verification::DocumentVerification::REJECTION_REASON_NAMES[activity.parameters[:reason]].downcase %>. 5 | <% end %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/public_activity/verification_aadhaar_verification/_create.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | started the Aadhaar flow. 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/verification_aadhaar_verification/_create_link.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | created a TruthScreen link in the Aadhaar flow. 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/verification_aadhaar_verification/_data_received.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | successfully imported their Aadhaar data. 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/verification_document_verification/_approve.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | approved <%= render activity.trackable.identity %>'s <%= link_to "verification", backend_verification_path(activity.trackable) %> 3 | <% unless activity.parameters[:ysws_eligible].nil? %> 4 | as YSWS <%= activity.parameters[:ysws_eligible] ? "eligible" : "ineligible" %>. 5 | <% end %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/public_activity/verification_document_verification/_create.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | uploaded a <%= activity.trackable.identity_document.document_type %> <%= link_to "document", backend_verification_path(activity.trackable) %>. 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/verification_document_verification/_ignored.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | marked a <%= link_to "verification", backend_verification_path(activity.trackable) %> as ignored because <%= activity.parameters[:reason] %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/verification_document_verification/_reject.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | rejected <%= render activity.trackable.identity %>'s <%= link_to "verification", backend_verification_path(activity.trackable) %> 3 | <% unless activity.parameters[:reason].nil? %> 4 | for <%= Verification::DocumentVerification::REJECTION_REASON_NAMES[activity.parameters[:reason]].downcase %>. 5 | <% end %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/public_activity/verification_document_verification/_update.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | edited a <%= activity.trackable.identity_document.document_type %> <%= link_to "document", backend_verification_path(activity.trackable) %>. 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/verification_vouch_verification/_create.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | created a vouch <%= link_to "verification", backend_verification_path(activity.trackable) %>. 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/public_activity/verification_vouch_verification/_ignored.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::PublicActivity::Snippet.new(activity) do %> 2 | marked a <%= link_to "vouch verification", backend_verification_path(activity.trackable) %> as ignored because <%= activity.parameters[:reason] %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/pwa/manifest.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IdentityVault", 3 | "icons": [ 4 | { 5 | "src": "/icon.png", 6 | "type": "image/png", 7 | "sizes": "512x512" 8 | }, 9 | { 10 | "src": "/icon.png", 11 | "type": "image/png", 12 | "sizes": "512x512", 13 | "purpose": "maskable" 14 | } 15 | ], 16 | "start_url": "/", 17 | "display": "standalone", 18 | "scope": "/", 19 | "description": "IdentityVault.", 20 | "theme_color": "red", 21 | "background_color": "red" 22 | } 23 | -------------------------------------------------------------------------------- /app/views/pwa/service-worker.js: -------------------------------------------------------------------------------- 1 | // Add a service worker for processing Web Push notifications: 2 | // 3 | // self.addEventListener("push", async (event) => { 4 | // const { title, options } = await event.data.json() 5 | // event.waitUntil(self.registration.showNotification(title, options)) 6 | // }) 7 | // 8 | // self.addEventListener("notificationclick", function(event) { 9 | // event.notification.close() 10 | // event.waitUntil( 11 | // clients.matchAll({ type: "window" }).then((clientList) => { 12 | // for (let i = 0; i < clientList.length; i++) { 13 | // let client = clientList[i] 14 | // let clientPath = (new URL(client.url)).pathname 15 | // 16 | // if (clientPath == event.notification.data.path && "focus" in client) { 17 | // return client.focus() 18 | // } 19 | // } 20 | // 21 | // if (clients.openWindow) { 22 | // return clients.openWindow(event.notification.data.path) 23 | // } 24 | // }) 25 | // ) 26 | // }) 27 | -------------------------------------------------------------------------------- /app/views/sessions/check_your_email.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "check your email..." %> 2 |
3 |
4 |

check your email...

5 |

we've sent a login link to your email address. please click the link to continue.

6 |

7 | Didn't receive an email? 8 | <%= link_to "Send another login link", new_sessions_path, class: "secondary" %> 9 | 10 |

11 | <% if Rails.env.development? %> 12 |

you're in dev! check 13 | <%= link_to "letter opener", letter_opener_web_path, target: "_blank" %>! 14 |

15 | <% end %> 16 |
17 |
18 | -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "Sign in" %> 2 |
3 |

Sign in

4 |
5 | <%= form_with url: sessions_path, method: :post, local: true do |form| %> 6 | <%= form.hidden_field :return_url, value: params[:return_url] if params[:return_url] %> 7 |
8 | <%= form.label :email, "Email address" %> 9 | <%= form.email_field :email, 10 | required: true, 11 | placeholder: "Enter your email address", 12 | value: session[:sign_in_email] %> 13 |
14 | <%= form.submit "Send login code!", style: "max-width: 200px;" %> 15 | <% end %> 16 |
17 |

18 | Don't have an account? 19 | <%= link_to "Get started here", basic_info_onboarding_path %> 20 |

21 |
22 | -------------------------------------------------------------------------------- /app/views/sessions/verify.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "Confirm Sign In" %> 2 |
3 |
4 |
5 |

6 | You're signing in as:
7 | <%= @login_code.identity.primary_email %> 8 |

9 |
10 | <%= form_with url: confirm_sessions_path, method: :post, local: true do |form| %> 11 | <%= form.hidden_field :token, value: @login_code.token %> 12 | <%= form.submit "Confirm Sign In", class: "primary" %> 13 | <% end %> 14 | 15 | Not you? 16 | <%= link_to "Request a new login link", new_sessions_path %> 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /app/views/shared/_flash.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |type, message| %> 2 | <% if message.present? %> 3 | <% alert_class = case type.to_sym 4 | when :success then 'success' 5 | when :notice, :info then 'info' 6 | when :warning then 'warning' 7 | when :alert, :error, :danger then 'danger' 8 | else 'info' 9 | end %> 10 | 27 | <% end %> 28 | <% end %> 29 | -------------------------------------------------------------------------------- /app/views/shared/async_flash.erb: -------------------------------------------------------------------------------- 1 | <% f.each do |type, message| %> 2 | <% if message.present? %> 3 | <% alert_class = case type.to_sym 4 | when :success then 'success' 5 | when :notice, :info then 'info' 6 | when :warning then 'warning' 7 | when :alert, :error, :danger then 'danger' 8 | else 'info' 9 | end %> 10 | 27 | <% end %> 28 | <% end %> -------------------------------------------------------------------------------- /app/views/static_pages/external_api_docs.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Components::Window.new("Identity Vault External API") do %> 2 |
3 | (click any of the URLs to copy them) 4 |
5 |
6 | 7 | Check an identity's status: 8 | 9 |
10 | 11 | <%= render Components::APIExample.new(method: "GET", url: api_external_check_url) %> 12 | Parameters are any of:
13 | <%= render Components::APIExample.new(method: "GET", url: api_external_check_url(email: "nora@hackclub.com"), path_only: true) %> 14 | <%= render Components::APIExample.new(method: "GET", url: api_external_check_url(slack_id: "U06QK6AG3RD"), path_only: true) %> 15 | <%= render Components::APIExample.new(method: "GET", url: api_external_check_url(idv_id: "ident!ZEOfPe"), path_only: true) %> 16 | 17 | Response will be of the shape:

18 |
{
19 |     "result": "{code}"
20 | }
21 |
22 | Possible results: 23 | 24 | 25 | 26 | 27 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
28 | Code 29 | 31 | Meaning 32 |
not_foundcouldn't find that identity
needs_submissionuser needs to submit or resubmit
pendingsubmitted, but not processed yet
rejectedduplicate identity or bad submission
verified_eligibleverified and YSWS eligible!
verified_but_over_18verified but not YSWS eligible
63 |
64 | <% end %> 65 | -------------------------------------------------------------------------------- /bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | ARGV.unshift("--ensure-latest") 6 | 7 | load Gem.bin_path("brakeman", "brakeman") 8 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | exec "./bin/rails", "server", *ARGV 3 | -------------------------------------------------------------------------------- /bin/docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Enable jemalloc for reduced memory usage and latency. 4 | if [ -z "${LD_PRELOAD+x}" ]; then 5 | LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) 6 | export LD_PRELOAD 7 | fi 8 | 9 | # If running the rails server then create or migrate existing database 10 | if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then 11 | ./bin/rails db:prepare 12 | fi 13 | 14 | exec "${@}" 15 | -------------------------------------------------------------------------------- /bin/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | echo "rubocoppin'..." 5 | bundle exec rubocop -A 6 | echo "erb_lint-in'?" 7 | bundle exec erb_lint app/views/ -a 8 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts "== Installing dependencies ==" 16 | system("bundle check") || system!("bundle install") 17 | 18 | # puts "\n== Copying sample files ==" 19 | # unless File.exist?("config/database.yml") 20 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 21 | # end 22 | 23 | puts "\n== Preparing database ==" 24 | system! "bin/rails db:prepare" 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! "bin/rails log:clear tmp:clear" 28 | 29 | unless ARGV.include?("--skip-server") 30 | puts "\n== Starting development server ==" 31 | STDOUT.flush # flush the output before exec(2) so that it displays 32 | exec "bin/dev" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /bin/thrust: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | load Gem.bin_path("thruster", "thrust") 6 | -------------------------------------------------------------------------------- /bin/vite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'vite' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("vite_ruby", "vite") 28 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/brakeman.ignore: -------------------------------------------------------------------------------- 1 | { 2 | "ignored_warnings": [ 3 | { 4 | "warning_type": "Dynamic Render Path", 5 | "warning_code": 15, 6 | "fingerprint": "60d8df7190a1ed518ea8679aa9c8d919f27fe7a6366669433833caa541f5040d", 7 | "check_name": "Render", 8 | "message": "Render path contains parameter value", 9 | "file": "app/views/backend/users/edit.html.erb", 10 | "line": 3, 11 | "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/", 12 | "code": "render(action => Backend::Users::Form.new(User.find(params[:id])), { :locals => ({ :\"backend::users::form\" => Backend::Users::Form.new(User.find(params[:id])) }) })", 13 | "render_path": [ 14 | { 15 | "type": "controller", 16 | "class": "Backend::UsersController", 17 | "method": "edit", 18 | "line": 17, 19 | "file": "app/controllers/backend/users_controller.rb", 20 | "rendered": { 21 | "name": "backend/users/edit", 22 | "file": "app/views/backend/users/edit.html.erb" 23 | } 24 | } 25 | ], 26 | "location": { 27 | "type": "template", 28 | "template": "backend/users/edit" 29 | }, 30 | "user_input": "params[:id]", 31 | "confidence": "Weak", 32 | "cwe_id": [ 33 | 22 34 | ], 35 | "note": "barring some bug in superform, this is fine" 36 | }, 37 | { 38 | "warning_type": "Redirect", 39 | "warning_code": 18, 40 | "fingerprint": "aedd908b558aa6308899ced4cdfa0918a074c38253b96d1ef06448762bd556be", 41 | "check_name": "Redirect", 42 | "message": "Possible unprotected redirect", 43 | "file": "app/controllers/slack_accounts_controller.rb", 44 | "line": 7, 45 | "link": "https://brakemanscanner.org/docs/warning_types/redirect/", 46 | "code": "redirect_to(Identity.slack_authorize_url(url_for(:action => :create, :only_path => false)), :host => \"https://slack.com\", :allow_other_host => true)", 47 | "render_path": null, 48 | "location": { 49 | "type": "method", 50 | "class": "SlackAccountsController", 51 | "method": "new" 52 | }, 53 | "user_input": "Identity.slack_authorize_url(url_for(:action => :create, :only_path => false))", 54 | "confidence": "Weak", 55 | "cwe_id": [ 56 | 601 57 | ], 58 | "note": "this will only ever be Slack" 59 | } 60 | ], 61 | "brakeman_version": "7.1.0" 62 | } 63 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | zo3UI/UqwQPGM+e23doyOyZFpQ+toWYutFpboBvWf3c5b+KiaUeFzwQ3X5F2maJRW+dgDmjy7YTMiRGehZ0bpoKfZg/v6f2A4FJhBfjMsUn14uziDlnJoKfcL3kz4H47pAEG9pTaMSo4Al8Z9+deRsK//thtDHLmBCE3kuI7NU7zf+kBio7zANNK/TzvHG+nzdoUMEdVV+UyjMBMAJpRsr6d+s2+RJyzVuxRkZ4mzn+BnCjVq6BprmEnNBktY9c8wsfX1DU0Bet8pzjVOJu47mFyzp1Z30e1cTG0WMA1wqNb9Wm+ny0zctriH5UkPFFMbHDPhlyX2r/+3aPTMziG/G5CE6RpnQ9J2GVkQe0+O8JWhZmZaE2GOfGNdSfk1Dnr3JKN8jFmufAD3+dfVXlbVbrA/YRgMlvomWU+9dTXMeEKuM3nnjF+A+0ljePtuX4EQqN/9UmaY+9LxaCPjKuAbvBYbFeIgZR3jNN0NOrVaxmEC90VPJ07hbfEaXys7xBP9blA0MU0b2tpptjOKE+UMybucSCSCZ+tYoBLgz3UygWoRJpLtBGLo0cluDkMbl7pNAVvfhN49nfzrnFwh95BnbPwd9fkYH+eKVhXr6Y5fu99bsBschN+nYdcejdfBBW7k0vrdnySNWEyZ7qEecJVCJuMod1Y3LysnN8REDFtYW39msIo65HjMhVwDRcz3B3ECE/nXVRzsV1yThUG5M3u24CxYbgwemjrothTBk68bTVm7ZWRRhXHhQT+tIt1T083CBNXOzmNxg86XUVRiiop3020EQAxK/k8bFosyz9gxVFkg5XiAvJGb1nK8cRAKs6Qpa5vLn3qVHjXTxDBLhIFIAJ4yMkMD/islArQbyVel0lkNm6L2I0wAE/SmZfn+Q86KS5rHPD5qg7brpLtiSMgklcj1+p9g59tDXZF9J4NlbBxlLnmHLpDeA1qK5bcqm6YIcN2pJNOsHtCZzUK1xI8sLCJdXEKztdwly5jgVxxsB3aYZymvdZbfXyyy3XsYHHEZHyVpS4xOLepzGddD/TJELIs3cvXjynWn/l58mhZNRZZHUVGRZXPE1D+uQFnMKNCeVHC8B4owdXGnD/TnZG3s50N/iJPN/b0Wog0mI/pIdGyMM+H2XpDEWgieBhW9kGBiWRMrzSJz8F3zEFM602AzvtdHQrE+WH7NEr5mNy0/hWgxL2SGpbySWjWbeXAXQDFH8Ovx1P/pmrbdEbsX6GTUCM=--ze+UL8nllGsLNhFb--20IqL5AlS4VjzgMzC3tFkg== -------------------------------------------------------------------------------- /config/credentials/production.yml.enc: -------------------------------------------------------------------------------- 1 | BurUUbLP11IDHWyiYRYz7FkogGjs+Pv9lhau25ucHYy2ud87j0YGvEuuHA0a5ajtFGhLwnP3uW5EAaIMSftch709K0gpuSZuKm2e4a2PPNNPiTIj068LslsfmqX2l2WDIuKwqZYMohzcrQxgB8xAxzyGJcTXXM8JI3ati8mD2pOur4S/Mfg9A1rCuXU42puPCwV7euQa/UAsuZsM+2BfL2biIVVYtPqJKXFd7/53hswRXLmylTa7ZG61EonDZvX21fBr4mFMOR8dQD5X4fS8rNH2NdVK4Fs1YcfFFw+UfHPYav9/NIwdzEJ9NCJ6qMOO7iimrxA0TVl8tvcymOaW+fiQ/ucaTwbf1JbLM+5W/EIKTHSbXMDqQzb1eQgivagKS8jN1Pit3YG1hkAFMgmjjuGX71coOWqPI8TLRgZ/Rq6UvLVWS6CmStHDA7fyPXRvyzTPey8LAWVwwhWxPxtt4bCw4z6D4qOi7OMWPD+dVuItkj/oSEd9LK3dsjHENhPEj3JTwcaT00LpJ6MQdnfd1EK54rFF0Fu6fCy0qY59ugsrP+leSoTNFGcDBhLHdgKFwR9bw6cT8fkrA7BkBss9UFNiz7LJ0ufGJt367H4hvuHHiWN9W2B8HkHEu7ES8v1xJCyxTcKO1ZdPF6GSiQzs6xjTajtBO+rf9uDl2Ey3YPpZqNKWeP9qu0Z45wksAIrBhTaPiS1JuDgfeHKoujo5D+eO/7ESdyMPuDTQlOqgPIPHhV4RJDW5OG9e/ZpF+ouSkYyWjxlWWbTWIyi1YLowoPdjNKRr7g0axwKSRyCscGXyC5Ww5DnUYRbtr7/PP8n2hWb8hIZ7vrjebYwyGzqKBE0PZdbbqgP0OuPCfXksh/NEs3Mrjfchs3xxLvzk7zrKUNP8epY88DduT5NxcOgo3u3k+wVdiXu5XCkVJw9JGySjr0vqt+3nes9Sc8zLrIPIhiCt4giX4Xa4NZ9CdHwT3n9BcX91lcZlRjrNAZeN8f7ewnkzNPESahXTJ5gaRHFF/Er85DmFJQYFGcR0gcAeE/oJlAb4wSEmoAMw2RBKIfFIG9lSzoeeGgJTIHTtADUY5JeYjWMBpqj/4qwqpspaO8p/WIz56xpd3YVBkbxqskfiaKi7zSCUGV1Cnbdwr9ZYs2xvBk51P/ReSUNFwS6PCzeBwal6o/DcttBG15kXP3AcZESyfENWqRI9OKoqsQ+hFP2FCaPO6iKHjiihvIt4S1DMnlEuhWQuZLqRcVafWBmHCA7Uvfpze3OsvZ8M7LypUwNEbfBjOYeK+yAmjJ8frEQMMjOME1wb3UwFfarKqSewHkLTlx7yBHcwbFoAtSRCz6ddA2SmI/Lcoa7t36tNGM8UJATpk4iApRwkeKvvPHWC0WDSkPeuvI6MBK6ByMjWNaR3Ys2njVRTRLz1YwM3YrTbz6uuyd6t0VVq95RAg5F2b73MIAbl4lzIK+clXvNXiU2wlzx7eo+Fm5bjiVnUydkdG6UBunU8k/CgSc80t3dFi6RhoqPTLxReREoCIU+y2DSFititK9Gagz6QMPJrFM2KbxkC74ALFR2X5WLkZub5ilaQGduTnn4RiGLUurZ6EGIb5g1GN9QzkKOLuJBucRWsdOLSq313gioIz6CMLpgA0mQFdKQ6WfoqDvW2V9zZnN+RTzH8rY5coBI2sEYO7EdFlMj+RFkXGWGkcydmFjDhQKka3iIgAHbFvfo53ks+cMIsg+d8Kz20o3pxiRjXm4peaS+zAhPRLNe/Jd0XrZokUk015KOBknT8y7V/BINxo7Og6p2mjXTThyghr2BUmV5aH9yosIMo4uUv+0+lPvHWFiQcYJlF1IfGtZZiquBxINUw--GDb+xqHfrmVhCNL3--jR4vs8c4CF5EgXdXMy873A== -------------------------------------------------------------------------------- /config/credentials/staging.yml.enc: -------------------------------------------------------------------------------- 1 | 5qMfsU4fxbkcUzYC52hpVq1uzVvsrW6GVf9gu7coXCcL2PInEtwBdqfEdyN1t9UdTkZApvOJP5NnFKHQurSCJsXFWjybmC3u6Lxvt0sltxYlPfUhFqyUACvv7B22T0NlesNKWK815GwzigIoxIlls7Vcy3nwCwc+2V9ymhvLSACy9070eIaQNisqtmST32K4Q3n5Wl1C2vQh/uRWwR5Uw21y+UtryZVNdlN//aWZHBcqkCtNvP2R20dv7Oy0RQ2eQ6l5y/OMUKcaEpbynwxNTMNJbRV4yEAMFf7NmXXAlsA/DwzaD0at/ywXBGrrvU65NC7cZ7XzSN+RmoV5Eche+P6+4MrqDnxJx9dUqH76ZUdx0ORPtRitc/KwRSZ2FfsGf5t1CTqIZrvm+v8g7+65EcCA6uqsqGCs2zFf2VOmNhP6ycf/bkCfciJgmFtRMBd5UEuZNovN0uNEYqFZyTDfNhYcGab4FX9DaTrNaYSltPmcjyAkWTUpM6Ve7008dFzG3p9mSpHv84kx+ffxD/+O7qVd3WZQt7EMs1JW+B6vflGyTMuBzdsuI/LcbDuqMfqaVfcZA9S6neth5e8Vx531pJYeUdV33mEYtdzr5i2r3giLH4+hjRbS+d2COdhvQXbYrQ/VYoV7qc5PzM0h2Iy0EY8wq7DoxyzCzE7iCHuv3HS/er+HaDr1ApvQifHG9YUCgX8KbrFxl47GjDW50TimWdZNmUyiRDiuZbJPBQMUBbbX99rEo5DO8CNN3lBL0JGP2+1eRt775W9tUGEeDN3vFx1HYaNYzVepZ1QaeawCA3BxlaYmN+rzh2fShBiCn48Majwl+eL0bmgDBlIQGZwTuGEDUmjY+TdT8ClAlpPsKu6S5f2j9+OGS/oXOAlCiELAxwqj5IZWYVXH2EPl6lOHxLRHpJke8/yVJwWqZInXwBxJ--EscHX+y20+os5i98--17TYWnHEc5miH/qc5tjlBg== -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/honeybadger.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # For more options, see https://docs.honeybadger.io/lib/ruby/gem-reference/configuration 3 | 4 | api_key: '<%= ENV["HONEYBADGER_API_KEY"] %>' 5 | 6 | # The environment your app is running in. 7 | env: "<%= Rails.env %>" 8 | 9 | # The absolute path to your project folder. 10 | root: "<%= Rails.root.to_s %>" 11 | 12 | # Honeybadger won't report errors in these environments. 13 | development_environments: 14 | - test 15 | - development 16 | - cucumber 17 | 18 | # By default, Honeybadger won't report errors in the development_environments. 19 | # You can override this by explicitly setting report_data to true or false. 20 | # report_data: true 21 | 22 | # The current Git revision of your project. Defaults to the last commit hash. 23 | # revision: null 24 | 25 | # Enable verbose debug logging (useful for troubleshooting). 26 | debug: false 27 | 28 | # Enable Honeybadger Insights 29 | insights: 30 | enabled: true 31 | rails: 32 | insights: 33 | metrics: true 34 | net_http: 35 | insights: 36 | metrics: true 37 | puma: 38 | insights: 39 | metrics: true 40 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # Allow @vite/client to hot reload javascript changes in development 15 | # policy.script_src *policy.script_src, :unsafe_eval, "http://#{ ViteRuby.config.host_with_port }" if Rails.env.development? 16 | 17 | # You may need to enable this in production as well depending on your setup. 18 | # policy.script_src *policy.script_src, :blob if Rails.env.test? 19 | 20 | # policy.style_src :self, :https 21 | # Allow @vite/client to hot reload style changes in development 22 | # policy.style_src *policy.style_src, :unsafe_inline if Rails.env.development? 23 | 24 | # # Specify URI for violation reports 25 | # # policy.report_uri "/csp-violation-report-endpoint" 26 | # end 27 | # 28 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 29 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 30 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 31 | # 32 | # # Report violations without enforcing the policy. 33 | # # config.content_security_policy_report_only = true 34 | # end 35 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/flipper.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | ## Memoization ensures that only one adapter call is made per feature per request. 3 | ## For more info, see https://www.flippercloud.io/docs/optimization#memoization 4 | # config.flipper.memoize = true 5 | 6 | ## Flipper preloads all features before each request, which is recommended if: 7 | ## * you have a limited number of features (< 100?) 8 | ## * most of your requests depend on most of your features 9 | ## * you have limited gate data combined across all features (< 1k enabled gates, like individual actors, across all features) 10 | ## 11 | ## For more info, see https://www.flippercloud.io/docs/optimization#preloading 12 | # config.flipper.preload = true 13 | 14 | ## Warn or raise an error if an unknown feature is checked 15 | ## Can be set to `:warn`, `:raise`, or `false` 16 | # config.flipper.strict = Rails.env.development? && :warn 17 | 18 | ## Show Flipper checks in logs 19 | # config.flipper.log = true 20 | 21 | ## Reconfigure Flipper to use the Memory adapter and disable Cloud in tests 22 | # config.flipper.test_help = true 23 | 24 | ## The path that Flipper Cloud will use to sync features 25 | # config.flipper.cloud_path = "_flipper" 26 | 27 | ## The instrumenter that Flipper will use. Defaults to ActiveSupport::Notifications. 28 | # config.flipper.instrumenter = ActiveSupport::Notifications 29 | end 30 | 31 | Flipper.configure do |config| 32 | ## Configure other adapters that you want to use here: 33 | ## See http://flippercloud.io/docs/adapters 34 | # config.use Flipper::Adapters::ActiveSupportCacheStore, Rails.cache, expires_in: 5.minutes 35 | end 36 | 37 | ## Register a group that can be used for enabling features. 38 | ## 39 | ## Flipper.enable_group :my_feature, :admins 40 | ## 41 | ## See https://www.flippercloud.io/docs/features#enablement-group 42 | # 43 | # Flipper.register(:admins) do |actor| 44 | # actor.respond_to?(:admin?) && actor.admin? 45 | # end 46 | -------------------------------------------------------------------------------- /config/initializers/git_version.rb: -------------------------------------------------------------------------------- 1 | # Get the first 6 characters of the current git commit hash 2 | git_hash = ENV["SOURCE_COMMIT"] || `git rev-parse HEAD` rescue "unknown" 3 | 4 | commit_link = git_hash != "unknown" ? "https://github.com/hackclub/identity-vault/commit/#{git_hash}" : nil 5 | 6 | short_hash = git_hash[0..7] 7 | 8 | commit_count = `git rev-list --count HEAD`.strip rescue 0 9 | 10 | # Check if there are any uncommitted changes 11 | is_dirty = `git status --porcelain`.strip.length > 0 rescue false 12 | 13 | # Append "-dirty" if there are uncommitted changes 14 | version = is_dirty ? "#{short_hash}-dirty" : short_hash 15 | 16 | # Store server start time 17 | Rails.application.config.server_start_time = Time.current 18 | 19 | # Store the version 20 | Rails.application.config.git_version = version 21 | Rails.application.config.git_commit_count = commit_count 22 | Rails.application.config.commit_link = commit_link 23 | -------------------------------------------------------------------------------- /config/initializers/good_job.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | config.good_job.cron = { 3 | expire_draft_aadhaar_verifications: { 4 | cron: "*/5 * * * *", # Run every 5 minutes 5 | class: "Verification::ExpireDraftAadhaarVerificationsJob" 6 | } 7 | } 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport::Inflector.inflections(:en) do |inflect| 2 | inflect.acronym "API" 3 | inflect.acronym "OAuth" 4 | inflect.acronym "HCB" 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/monkey_patches.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.to_prepare do 2 | class ActiveStorage::Blob 3 | before_validation :generate_encryption_key, on: :create 4 | 5 | private 6 | 7 | def generate_encryption_key 8 | self.encryption_key ||= SecureRandom.bytes(48) 9 | end 10 | end 11 | 12 | class Doorkeeper::AuthorizationsController 13 | before_action :hide_some_data_away, only: :new 14 | end 15 | 16 | class Doorkeeper::RedirectUriValidator 17 | def validate_each(record, attribute, value) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/initializers/paper_trail.rb: -------------------------------------------------------------------------------- 1 | class PaperTrail::Version 2 | def responsible_party 3 | return nil unless whodunnit.present? 4 | if whodunnit&.start_with? "Backend user: " 5 | uid = whodunnit[14..] 6 | return nil unless uid.present? 7 | Backend::User.find_by(id: uid) 8 | else 9 | Identity.find_by(id: whodunnit) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/initializers/phlex.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Views 4 | end 5 | 6 | module Components 7 | extend Phlex::Kit 8 | end 9 | 10 | Rails.autoloaders.main.push_dir( 11 | Rails.root.join("app/views"), namespace: Views 12 | ) 13 | 14 | Rails.autoloaders.main.push_dir( 15 | Rails.root.join("app/components"), namespace: Components 16 | ) 17 | -------------------------------------------------------------------------------- /config/initializers/public_activity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | PublicActivity.enabled = true 4 | -------------------------------------------------------------------------------- /config/initializers/slack.rb: -------------------------------------------------------------------------------- 1 | Slack.configure do |config| 2 | config.token = ENV["SLACK_BOT_TOKEN"] 3 | end 4 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization and 2 | # are automatically loaded by Rails. If you want to use locales other than 3 | # English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more about the API, please read the Rails Internationalization guide 20 | # at https://guides.rubyonrails.org/i18n.html. 21 | # 22 | # Be aware that YAML interprets the following case-insensitive strings as 23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings 24 | # must be quoted to be interpreted as strings. For example: 25 | # 26 | # en: 27 | # "yes": yup 28 | # enabled: "ON" 29 | 30 | en: 31 | activerecord: 32 | attributes: 33 | backend_user: 34 | slack_id: Slack ID 35 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # This configuration file will be evaluated by Puma. The top-level methods that 2 | # are invoked here are part of Puma's configuration DSL. For more information 3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 4 | # 5 | # Puma starts a configurable number of processes (workers) and each process 6 | # serves each request in a thread from an internal thread pool. 7 | # 8 | # You can control the number of workers using ENV["WEB_CONCURRENCY"]. You 9 | # should only set this value when you want to run 2 or more workers. The 10 | # default is already 1. 11 | # 12 | # The ideal number of threads per worker depends both on how much time the 13 | # application spends waiting for IO operations and on how much you wish to 14 | # prioritize throughput over latency. 15 | # 16 | # As a rule of thumb, increasing the number of threads will increase how much 17 | # traffic a given process can handle (throughput), but due to CRuby's 18 | # Global VM Lock (GVL) it has diminishing returns and will degrade the 19 | # response time (latency) of the application. 20 | # 21 | # The default is set to 3 threads as it's deemed a decent compromise between 22 | # throughput and latency for the average Rails application. 23 | # 24 | # Any libraries that use a connection pool or another resource pool should 25 | # be configured to provide at least as many connections as the number of 26 | # threads. This includes Active Record's `pool` parameter in `database.yml`. 27 | threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) 28 | threads threads_count, threads_count 29 | 30 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 31 | port ENV.fetch("PORT", 3000) 32 | 33 | # Allow puma to be restarted by `bin/rails restart` command. 34 | plugin :tmp_restart 35 | 36 | # Specify the PID file. Defaults to tmp/pids/server.pid in development. 37 | # In other environments, only set the PID file if requested. 38 | pidfile ENV["PIDFILE"] if ENV["PIDFILE"] 39 | -------------------------------------------------------------------------------- /config/sanctioned_countries.yml: -------------------------------------------------------------------------------- 1 | shared: 2 | - CU 3 | - IR 4 | - KP 5 | - SY -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | encrypted_local_disk: 10 | service: EncryptedDisk 11 | private_url_policy: stream 12 | root: <%= ENV["ENCRYPTED_LOCAL_DISK_ROOT"] || Rails.root.join("storage", "encrypted") %> 13 | 14 | #prod_id_documents: 15 | # service: EncryptedS3 16 | # endpoint: "https://hel1.your-objectstorage.com" 17 | # access_key_id: <%#= Rails.application.credentials.dig(:hetzner, :access_key_id) %> 18 | # secret_access_key: <%#= Rails.application.credentials.dig(:hetzner, :secret_access_key) %> 19 | # region: hel1 20 | # bucket: hackclub-identity-docs-prod 21 | # private_url_policy: stream 22 | 23 | prod_id_documents: 24 | service: EncryptedS3 25 | endpoint: <%= Rails.application.credentials.dig(:cloudflare, :endpoint) %> 26 | access_key_id: <%= Rails.application.credentials.dig(:cloudflare, :access_key_id) %> 27 | secret_access_key: <%= Rails.application.credentials.dig(:cloudflare, :secret_access_key) %> 28 | bucket: hackclub-identity-docs-prod 29 | region: auto 30 | private_url_policy: stream 31 | request_checksum_calculation: "when_required" 32 | response_checksum_validation: "when_required" -------------------------------------------------------------------------------- /config/vite.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": { 3 | "sourceCodeDir": "app/frontend", 4 | "watchAdditionalPaths": ["app/views/**/*.html.erb", "app/frontend/**/*.scss"], 5 | "packageManager": "yarn" 6 | }, 7 | "development": { 8 | "autoBuild": true, 9 | "publicOutputDir": "vite-dev", 10 | "port": 3036 11 | }, 12 | "test": { 13 | "autoBuild": true, 14 | "publicOutputDir": "vite-test", 15 | "port": 3037 16 | }, 17 | "production": { 18 | "publicOutputDir": "", 19 | "port": 3038 20 | }, 21 | "staging": { 22 | "publicOutputDir": "", 23 | "port": 3039 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /db/migrate/20250902193412_add_extra_data_to_versions.rb: -------------------------------------------------------------------------------- 1 | class AddExtraDataToVersions < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :versions, :extra_data, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should ensure the existence of records required to run the application in every environment (production, 2 | # development, test). The code here should be idempotent so that it can be executed at any point in every environment. 3 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 4 | # 5 | # Example: 6 | # 7 | # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| 8 | # MovieGenre.find_or_create_by!(name: genre_name) 9 | # end 10 | -------------------------------------------------------------------------------- /docker-compose-dbonly.yml: -------------------------------------------------------------------------------- 1 | # To use this DB-only ("dockerless") setup, pass a `-f docker-compose.dbonly.yml` to docker compose. 2 | # e.g. `docker compose -f docker-compose.dbonly.yml up` 3 | services: 4 | db: 5 | image: "postgres:11.16" 6 | volumes: 7 | - pg-data:/var/lib/postgresql/data 8 | environment: 9 | POSTGRES_HOST_AUTH_METHOD: trust 10 | ports: 11 | - 5432:5432 12 | redis: 13 | image: redis 14 | volumes: 15 | - redis-data:/data 16 | ports: 17 | - 6379:6379 18 | 19 | volumes: 20 | pg-data: 21 | redis-data: 22 | -------------------------------------------------------------------------------- /lib/application_component.rb: -------------------------------------------------------------------------------- 1 | class ApplicationComponent < Components::Base 2 | end 3 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/log/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "identity-vault", 3 | "private": true, 4 | "devDependencies": { 5 | "@csstools/postcss-sass": "^5.1.1", 6 | "@noble/secp256k1": "^2.2.3", 7 | "autoprefixer": "^10.4.21", 8 | "postcss": "^8.5.3", 9 | "postcss-import": "^16.1.0", 10 | "postcss-nested": "^7.0.2", 11 | "postcss-sass": "^0.5.0", 12 | "postcss-scss": "^4.0.9", 13 | "rollup": "^4.41.0", 14 | "sass-embedded": "^1.89.0", 15 | "vite": "^5.0.0", 16 | "vite-plugin-rails": "^0.5.0", 17 | "vite-plugin-ruby": "^5.1.0" 18 | }, 19 | "dependencies": { 20 | "@noble/curves": "^1.9.1", 21 | "@picocss/pico": "^2.1.1", 22 | "@rails/activestorage": "^8.0.200", 23 | "alpinejs": "^3.14.9", 24 | "axios": "^1.9.0", 25 | "dreamland": "^0.0.25", 26 | "htmx.org": "^1.9.12", 27 | "jquery": "^3.7.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/.well-known/security.txt: -------------------------------------------------------------------------------- 1 | Contact: https://security.hackclub.com 2 | Contact: mailto:nora@hackclub.com 3 | Expires: 2026-11-27T05:00:00.000Z 4 | Acknowledgments: https://bugs.hackclub.com/hall-of-fame.php 5 | Preferred-Languages: en 6 | Canonical: https://identity.hackclub.com/.well-known/security.txt 7 | Policy: https://security.hackclub.com/ 8 | -------------------------------------------------------------------------------- /public/ChicagoFLF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/public/ChicagoFLF.ttf -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/public/icon.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 19 | 37 | 41 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /script/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/script/.keep -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/storage/.keep -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/tmp/.keep -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/tmp/storage/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/identity-vault/ecb8134e8fdb34c3de417e250e0dab0911116bc3/vendor/.keep -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import ViteRails from "vite-plugin-rails"; 3 | import tailwindcss from '@tailwindcss/vite' 4 | export default defineConfig({ 5 | plugins: [ 6 | ViteRails({ 7 | envVars: { RAILS_ENV: "development" }, 8 | envOptions: { defineOn: "import.meta.env" }, 9 | fullReload: { 10 | additionalPaths: ["config/routes.rb", "app/views/**/*"], 11 | delay: 300, 12 | }, 13 | }), 14 | // tailwindcss(), 15 | ], 16 | build: { sourcemap: false }, 17 | }); --------------------------------------------------------------------------------