mirror of
https://github.com/System-End/identity-vault.git
synced 2026-04-19 15:18:23 +00:00
better DX (#168)
This commit is contained in:
parent
bec5543cbf
commit
93c8fa990f
8 changed files with 255 additions and 32 deletions
168
README.md
168
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
10
config/initializers/active_record_encryption.rb
Normal file
10
config/initializers/active_record_encryption.rb
Normal 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
|
||||
11
config/initializers/lockbox.rb
Normal file
11
config/initializers/lockbox.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
82
db/seeds.rb
82
db/seeds.rb
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue