better DX (#168)

This commit is contained in:
nora 2026-01-20 23:10:23 -05:00 committed by GitHub
parent bec5543cbf
commit 93c8fa990f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 255 additions and 32 deletions

168
README.md
View file

@ -10,25 +10,163 @@ avoid questions that can be answered by reading the source code, but otherwise i
kindly `bin/lint` your code before you submit it!
### areas of focus
## local dev setup
the ops view components (look in `app/components`) are a hot mess...
### prerequisites
so is the onboarding controller, she should really be ripped out and replaced.
you'll need:
- ruby 3.4.4+ (i use [mise](https://mise.jdx.dev) to manage this)
- node.js + yarn
- postgres (see below)
- imagemagick & libvips (image processing)
- libxmlsec1 (SAML signing)
## dev setup
on macOS:
- make sure you have working installations of ruby ≥ 3.4.4 & nodejs
- clone repo
- create .env.development, populate `DATABASE_URL` w/ a local postgres instance and `LOCKBOX_MASTER_KEY` with the value of `openssl rand -hex 32`
- 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`
- if you don't have docker and are on macOS, [orbstack](https://orbstack.dev) may be helpful
- run `bundle install`
- run `bin/rails db:prepare`
- console in (`bin/rails console`)
- `Backend::User.create!(slack_id: "U<whatever>", username: "<you>", active: true, super_admin: true)`
- run `bin/dev` (and `bin/vite dev` if you want hot reload on css & js)
- visit `http://localhost:3000/backend/login`, paste that Slack ID in, and "fake it til' you make it"
```bash
brew install imagemagick libvips libxmlsec1 yarn
```
### database
easiest way is docker. if you don't have it and you're on macOS, [orbstack](https://orbstack.dev) works well enough.
```bash
docker compose -f docker-compose-dbonly.yml up -d
```
this gives you a postgres instance at `postgresql://postgres@localhost:5432/identity_vault_development`.
if you've got your own postgres running somewhere, that works too just point at it.
### environment
create a `.env.development` file:
```bash
DATABASE_URL=postgresql://postgres@localhost:5432/identity_vault_development
```
that's it for local dev lockbox will use a deterministic dev key automatically. see [environment variables](#environment-variables) below for the full list.
### install & setup
```bash
bundle install
yarn install
bin/rails db:prepare
bin/rails db:seed
```
the seeds create a dev account with 2FA already set up. it'll print out the TOTP secret add that to your authenticator app.
### running the thing
```bash
bin/dev
```
if you want hot reload on css & js, also run `bin/vite dev` in another terminal.
### logging in to the backend
1. go to http://localhost:3000/login
2. enter `identity@hackclub.com`
3. grab the verification code from http://localhost:3000/letter_opener
4. enter your TOTP code (from the authenticator app you set up during seeding)
5. head to http://localhost:3000/backend
the backend requires 2FA that's why the seeds set up a TOTP for you.
## environment variables
### required
| var | description |
|-----|-------------|
| `DATABASE_URL` | postgres connection string |
### required in production
| var | description |
|-----|-------------|
| `SECRET_KEY_BASE` | rails secret key generate with `openssl rand -hex 64` |
| `LOCKBOX_MASTER_KEY` | encryption key for lockbox fields generate with `openssl rand -hex 32` |
### active record encryption
used for `encrypts` fields (like aadhaar data). generate these with `bin/rails db:encryption:init` or use random strings.
| var | description |
|-----|-------------|
| `ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY` | primary encryption key |
| `ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY` | deterministic encryption key |
| `ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT` | key derivation salt |
### slack integration
| var | description |
|-----|-------------|
| `SLACK_BOT_TOKEN` | bot token (xoxb-*) |
| `SLACK_TEAM_ID` | workspace ID (T*) |
| `SLACK_SCIM_TOKEN` | SCIM API token for user provisioning |
| `SLACK_CLIENT_ID` | OAuth client ID |
| `SLACK_CLIENT_SECRET` | OAuth client secret |
| `SLACK_SIGNING_SECRET` | webhook request verification |
| `SLACK_ADULT_WEBHOOK_URL` | webhook for guardian notifications |
### SAML
| var | description |
|-----|-------------|
| `SAML_IDP_CERT_PATH` | path to SAML IdP certificate |
| `SAML_IDP_KEY_PATH` | path to SAML IdP private key |
generate a self-signed cert for local dev:
```bash
openssl req -x509 -newkey rsa:2048 -keyout saml_key.pem -out saml_cert.pem -days 365 -nodes -subj "/CN=localhost"
```
### OIDC
| var | description |
|-----|-------------|
| `OIDC_SIGNING_KEY` | RSA private key for JWT signing |
generate an RSA key:
```bash
openssl genrsa -out oidc_key.pem 2048
```
then set `OIDC_SIGNING_KEY` to the contents of `oidc_key.pem` (the whole thing including the BEGIN/END lines).
### email (production/staging/uat)
| var | description |
|-----|-------------|
| `SES_SMTP_HOST` | SES SMTP endpoint |
| `SES_SMTP_USERNAME` | SES SMTP username |
| `SES_SMTP_PASSWORD` | SES SMTP password |
### document storage (production)
| var | description |
|-----|-------------|
| `CLOUDFLARE_R2_ENDPOINT` | R2 endpoint URL |
| `CLOUDFLARE_R2_ACCESS_KEY_ID` | R2 access key |
| `CLOUDFLARE_R2_SECRET_ACCESS_KEY` | R2 secret key |
### other
| var | description |
|-----|-------------|
| `SENTRY_DSN` | error tracking |
| `GOOGLE_PLACES_API_KEY` | address autocomplete |
| `ANALYTICS_DATABASE_URL` | separate analytics DB (optional) |
| `DISABLE_ANALYTICS` | set to "true" to disable Ahoy |
| `SOURCE_COMMIT` | git commit for version display |
## security

View file

@ -64,7 +64,7 @@ module ApplicationHelper
end
def google_places_api_script_tag
api_key = Rails.application.credentials.dig(:google, :places_api_key)
api_key = ENV["GOOGLE_PLACES_API_KEY"]
return unless api_key.present?
tag.script(src: "https://maps.googleapis.com/maps/api/js?key=#{api_key}&loading=async&libraries=places&callback=onGoogleMapsLoaded", async: true, defer: true)

View file

@ -29,7 +29,7 @@ class Slack::NotifyGuardiansJob < ApplicationJob
verf = identity.latest_verification
context_line = "*ref:* <#{backend_identity_url(identity)}|#{identity.public_id}> / <#{backend_verification_url(verf)}|#{verf.public_id}>"
HTTP.post(Rails.application.credentials.slack.adult_webhook, body: {
HTTP.post(ENV["SLACK_ADULT_WEBHOOK_URL"], body: {
"blocks": [
{
"type": "section",

View file

@ -62,10 +62,10 @@ Rails.application.configure do
# Amazon SES SMTP settings
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: Rails.application.credentials.dig(:ses, :smtp_host) || "email-smtp.us-east-1.amazonaws.com",
address: ENV["SES_SMTP_HOST"],
port: 587,
user_name: Rails.application.credentials.dig(:ses, :smtp_username),
password: Rails.application.credentials.dig(:ses, :smtp_password),
user_name: ENV["SES_SMTP_USERNAME"],
password: ENV["SES_SMTP_PASSWORD"],
authentication: "plain",
enable_starttls: true
}

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Configure Active Record Encryption to use environment variables
# instead of Rails credentials
if ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"].present?
Rails.application.config.active_record.encryption.primary_key = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"]
Rails.application.config.active_record.encryption.deterministic_key = ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"]
Rails.application.config.active_record.encryption.key_derivation_salt = ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"]
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
if ENV["LOCKBOX_MASTER_KEY"].present?
Lockbox.master_key = ENV["LOCKBOX_MASTER_KEY"]
elsif Rails.env.development? || Rails.env.test?
# generate a deterministic key for dev/test so encrypted data persists across restarts
# this is NOT secure for production always set LOCKBOX_MASTER_KEY in prod
Lockbox.master_key = Digest::SHA256.hexdigest("hca-dev-key")
else
raise "LOCKBOX_MASTER_KEY must be set in production"
end

View file

@ -22,9 +22,9 @@ encrypted_local_disk:
prod_id_documents:
service: EncryptedS3
endpoint: <%= Rails.application.credentials.dig(:cloudflare, :endpoint) %>
access_key_id: <%= Rails.application.credentials.dig(:cloudflare, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:cloudflare, :secret_access_key) %>
endpoint: <%= ENV["CLOUDFLARE_R2_ENDPOINT"] %>
access_key_id: <%= ENV["CLOUDFLARE_R2_ACCESS_KEY_ID"] %>
secret_access_key: <%= ENV["CLOUDFLARE_R2_SECRET_ACCESS_KEY"] %>
bucket: hackclub-identity-docs-prod
region: auto
private_url_policy: stream

View file

@ -1,9 +1,73 @@
# This file should ensure the existence of records required to run the application in every environment (production,
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
#
# Example:
#
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
# MovieGenre.find_or_create_by!(name: genre_name)
# end
# frozen_string_literal: true
# Seeds for local development
# Run with: bin/rails db:seed
return unless Rails.env.development?
puts "🌱 seeding development data..."
# Create a test identity
identity = Identity.find_or_initialize_by(primary_email: "identity@hackclub.com")
if identity.new_record?
identity.assign_attributes(
first_name: "Dev",
last_name: "User",
birthday: Date.new(2000, 1, 1),
country: :US
)
identity.save!
puts " created identity: #{identity.primary_email}"
else
puts " identity already exists: #{identity.primary_email}"
end
# Create or find a verified TOTP for backend access
totp = identity.totps.verified.first || identity.totps.find_by(aasm_state: "unverified")
if totp.nil?
totp = identity.totps.create!(aasm_state: "verified")
puts " created TOTP for 2FA"
elsif totp.aasm_state != "verified"
totp.update!(aasm_state: "verified")
puts " verified existing TOTP"
else
puts " TOTP already exists and verified"
end
# Create backend user with super admin
backend_user = identity.backend_user || identity.build_backend_user
if backend_user.new_record?
backend_user.assign_attributes(
username: "dev",
active: true,
super_admin: true
)
backend_user.save!
puts " created backend user with super_admin"
else
puts " backend user already exists"
end
puts ""
puts "=" * 60
puts "dev account ready!"
puts "=" * 60
puts ""
puts " email: identity@hackclub.com"
puts " totp secret: #{totp.secret}"
puts ""
puts "add this secret to your authenticator app, or use this URI:"
puts ""
puts " #{totp.provisioning_uri}"
puts ""
puts "login flow:"
puts " 1. go to http://localhost:3000/login"
puts " 2. enter: identity@hackclub.com"
puts " 3. grab the code from http://localhost:3000/letter_opener"
puts " 4. enter the TOTP code from your authenticator"
puts " 5. go to http://localhost:3000/backend"
puts ""
puts "=" * 60