diff --git a/.gitignore b/.gitignore index 4beb847..a290715 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ dist/ *.log .env .env.* +!.env.example +!*/.env.example +!**/.env.example .gstack/ apps/store-server/data/ apps/hq/data/ diff --git a/apps/hq/.env.example b/apps/hq/.env.example new file mode 100644 index 0000000..6636ba9 --- /dev/null +++ b/apps/hq/.env.example @@ -0,0 +1,34 @@ +# SiMS HQ console — environment. Copy to `.env` (gitignored) and fill in. +# Nothing here is required to BOOT (the app runs with none set), but each unlocks +# a capability. Never commit the filled-in file. + +# --- Core --- +# Where the SQLite database lives (created on first boot). On the AWS box use an +# absolute path on a backed-up volume, e.g. /var/lib/sims-hq/data +HQ_DATA_DIR=/var/lib/sims-hq/data +# Port the console listens on (default 5182). +HQ_PORT=5182 + +# --- Security (set BEFORE first real login) --- +# Server-held pepper mixed into every password/PIN hash. Without it, a stolen DB +# can be brute-forced. Generate once, keep secret, NEVER change after users exist: +# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +SIMS_PIN_PEPPER= +# Encrypts the stored Gmail refresh token (AES-256-GCM). 64 hex chars: +# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +HQ_SECRET_KEY= + +# --- Gmail sending (quotations, reminders) --- +# From a Google Cloud project (OAuth client, app published to Production). After +# setting these, run: npx tsx apps/hq/scripts/gmail-connect.ts (one-time consent). +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# --- AWS cost-per-client (HQ-3) --- +# A read-only IAM user with Cost Explorer access. When both are set, the scheduler +# pulls monthly cost grouped by the 'client' cost-allocation tag. +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# --- Postgres (go-live; not yet wired — see docs/DEPLOY-HQ.md) --- +# HQ_PG_URL=postgres://hq:PASSWORD@your-rds-endpoint:5432/hq diff --git a/apps/hq/scripts/backup-hq.sh b/apps/hq/scripts/backup-hq.sh new file mode 100644 index 0000000..c5e7496 --- /dev/null +++ b/apps/hq/scripts/backup-hq.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Nightly backup of the HQ SQLite database to S3 (and a local rotation). +# Usage: HQ_DATA_DIR=/var/lib/sims-hq/data S3_BUCKET=s3://your-bucket/hq-backups ./backup-hq.sh +# Cron (2am daily): 0 2 * * * /opt/sims-hq/apps/hq/scripts/backup-hq.sh >> /var/log/sims-hq-backup.log 2>&1 +set -euo pipefail + +DATA_DIR="${HQ_DATA_DIR:-/var/lib/sims-hq/data}" +DB="$DATA_DIR/hq.db" +STAMP="$(date +%Y%m%d-%H%M%S)" +OUT="$DATA_DIR/backups" +mkdir -p "$OUT" + +if [ ! -f "$DB" ]; then echo "no db at $DB"; exit 1; fi + +# Consistent snapshot even while the app is running (WAL-safe). +sqlite3 "$DB" ".backup '$OUT/hq-$STAMP.db'" +gzip -f "$OUT/hq-$STAMP.db" +echo "local backup: $OUT/hq-$STAMP.db.gz" + +# Off-box copy (recommended). Requires awscli + an IAM role/keys with s3:PutObject. +if [ -n "${S3_BUCKET:-}" ]; then + aws s3 cp "$OUT/hq-$STAMP.db.gz" "$S3_BUCKET/hq-$STAMP.db.gz" + echo "uploaded: $S3_BUCKET/hq-$STAMP.db.gz" +fi + +# Keep the last 14 local snapshots. +ls -1t "$OUT"/hq-*.db.gz 2>/dev/null | tail -n +15 | xargs -r rm -f +echo "backup ok" diff --git a/docs/DEPLOY-HQ.md b/docs/DEPLOY-HQ.md new file mode 100644 index 0000000..f7e9ff2 --- /dev/null +++ b/docs/DEPLOY-HQ.md @@ -0,0 +1,89 @@ +# Go-Live Runbook — HQ ops console (`apps/hq`) + +How to put the internal console on your AWS box for real use. The store product is +unaffected (it stays local, D14); this is the vendor-side cloud app doc 11 always +prescribed. Today the engine is SQLite; the Postgres switch is called out where it lands. + +## What you provide (the only blockers) +1. **A server** — a small AWS EC2 instance (t3.small is plenty for 5 users), Ubuntu 22.04+, + with a domain or Elastic IP and HTTPS (via a reverse proxy — step 4). +2. **A GitHub repo** — so the box can pull the code (or we copy a build artifact). +3. Later, per capability: a Google Cloud OAuth app (Gmail), a read-only AWS IAM user + (cost data), and your **APEX CSV export** (the 300 clients). + +## 1. Get the code on the box +```bash +sudo mkdir -p /opt/sims-hq && sudo chown $USER /opt/sims-hq +git clone /opt/sims-hq # after `gh repo create sims-next` + push +cd /opt/sims-hq +# Node 20+ (nvm or NodeSource). Then: +npm install +``` + +## 2. Configure +```bash +cp apps/hq/.env.example apps/hq/.env +# Fill at minimum SIMS_PIN_PEPPER and HQ_SECRET_KEY (generators are in the file). +# Set HQ_DATA_DIR to a path on a backed-up volume, e.g. /var/lib/sims-hq/data. +``` +`SIMS_PIN_PEPPER` **must be set before the first real login** and never changed after +(changing it invalidates every password). `HQ_SECRET_KEY` must be set before connecting +Gmail. + +## 3. Build + run under a process manager +```bash +cd apps/hq-web && npm run build && cd ../.. # builds the web UI the server serves +# First boot prints a one-time owner password — capture it from the logs. +env $(grep -v '^#' apps/hq/.env | xargs) node apps/hq/dist/server.cjs # smoke test, Ctrl-C + +# Keep it running with pm2 (or a systemd unit): +npm i -g pm2 +cd apps/hq && pm2 start "npm start" --name sims-hq && pm2 save && pm2 startup +``` +Owner login on first boot is `admin@tecnostac.com` + the printed password (change it +immediately). The Gmail-disconnected banner is expected until step 6. + +## 4. Put HTTPS in front (never expose Node directly) +Reverse-proxy 443 → `localhost:5182` with Caddy (simplest) or nginx + certbot: +``` +# Caddyfile +hq.yourdomain.com { + reverse_proxy localhost:5182 +} +``` +Lock the EC2 security group so only 443 is public; 5182 stays localhost-only. The public +`/share/:token` links are served through this same proxy — that's intended (they're the +one unauthenticated route, token-guarded, rate-limited, revocable). + +## 5. Backups (do this on day one) +```bash +chmod +x apps/hq/scripts/backup-hq.sh +# Nightly to S3 + 14-day local rotation: +crontab -e +0 2 * * * HQ_DATA_DIR=/var/lib/sims-hq/data S3_BUCKET=s3://your-bucket/hq-backups /opt/sims-hq/apps/hq/scripts/backup-hq.sh >> /var/log/sims-hq-backup.log 2>&1 +``` +Do a **restore drill** once: copy a backup to a scratch box and boot the app against it. + +## 6. Connect the capabilities (each optional, each unlocks a feature) +- **Gmail** (sending): create a Google Cloud OAuth client, publish the app to Production, + set `GOOGLE_CLIENT_ID/SECRET` + `HQ_SECRET_KEY`, then `npx tsx apps/hq/scripts/gmail-connect.ts` + and grant consent as the company mailbox. The banner clears; auto-reminders start sending. +- **AWS cost per client**: create a read-only IAM user with Cost Explorer access, tag each + client's cloud resources with a `client` cost-allocation tag, set `AWS_*`. The scheduler + pulls monthly. +- **Your 300 clients**: export `clients.csv` + `invoices.csv` from APEX into `import-drop/`, + then run the importer (verification report first, `--commit` after you approve). + +## 7. The Postgres switch (before or at go-live) +Decision D15 locks Postgres as the production engine. It is a **dedicated engineering task, +not a config flag** — the repositories move from synchronous SQLite to async Postgres +behind the same interfaces, validated against your RDS instance. Provide the RDS endpoint + +a dedicated empty `hq` database and a scoped user (`HQ_PG_URL`), and it is built and tested +against that target, then the 300 clients import directly into Postgres — data never +migrates engines mid-life. Until then the app runs correctly on SQLite with the backups above. + +## Health & operations +- `GET /api/health` → `{ ok: true }` for your uptime monitor. +- Logs: `pm2 logs sims-hq`. The scheduler runs on boot and every 6h (reminders, bounces, + AWS pull when configured). +- Every mutation is audit-logged in the DB; the public share route and previews write nothing.