From 143582458e41ee48730c9a7edc9d3f91983fe432 Mon Sep 17 00:00:00 2001 From: Ulas Kalayci Date: Tue, 21 Apr 2026 12:49:45 +0200 Subject: [PATCH] docs(installer): add reconnaissance findings and implementation plan --- docs/installer-plan.md | 116 ++++++++++++++++++++++++++++++++++++++++ docs/installer-recon.md | 97 +++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 docs/installer-plan.md create mode 100644 docs/installer-recon.md diff --git a/docs/installer-plan.md b/docs/installer-plan.md new file mode 100644 index 0000000..d829363 --- /dev/null +++ b/docs/installer-plan.md @@ -0,0 +1,116 @@ +# Installer Implementation Plan + +## Phase 0 Findings Summary + +See [installer-recon.md](installer-recon.md) for full details. + +Key finding: **A new `POST /api/v1/auth/setup` endpoint is required** to allow first-admin creation via HTTP when the app runs in Docker. Both installers depend on this. + +## Dependency Graph + +``` +[1] Setup endpoint (server/auth.js) + ↓ +[2a] CLI installer (install.sh) [2b] Web installer (tools/installer/) + ↓ ↓ +[3] .dockerignore update + docs +``` + +Steps 2a and 2b are independent and can be built in parallel, but both depend on Step 1. + +## Deliverables + +### 1. Setup Bootstrap Endpoint (blocking — ~1h, complexity: low) + +**File**: `server/auth.js` — add new route before the auth guard. + +``` +POST /api/v1/auth/setup +``` + +Behavior: +- Query `SELECT COUNT(*) FROM users` — if > 0, return `403 { error: "Setup already completed", code: 403 }` +- Validate input: `username` (3-64 chars, alphanumeric + `._-`), `display_name` (1-128 chars), `password` (min 8 chars) +- Hash password with bcrypt (cost 12), insert user with `role: 'admin'` +- Return `201 { user: { id, username, display_name, avatar_color, role } }` +- Rate-limited (reuse existing `loginLimiter` or a custom one) +- No session/CSRF required (unauthenticated endpoint) +- Mounted at `/api/v1/auth/setup` in `server/index.js` (before the `requireAuth` middleware) + +**Test**: Add `test:setup` script. Verify: creates admin when no users exist, returns 403 when users exist, validates input. + +### 2a. CLI Installer — `install.sh` (~3h, complexity: medium) + +**File**: `install.sh` in repository root. + +Wizard steps (7 total): +1. **Prerequisites check**: Docker, docker compose, openssl (or /dev/urandom fallback), curl, jq (optional, graceful fallback) +2. **Basic config**: domain/IP (default: `localhost`), port (default: `3000`), timezone +3. **Security secrets**: SESSION_SECRET + DB_ENCRYPTION_KEY — each with [G]enerate / [M]anual +4. **Weather** (optional): ask if wanted, prompt for API key +5. **Calendar** (optional): Google OAuth / Apple CalDAV, each skippable +6. **Review & launch**: display masked .env, confirm, write `.env`, run `docker compose up -d`, poll `/health` every 2s (120s timeout) +7. **Admin creation**: prompt username, display_name, password (read -s), POST to `/api/v1/auth/setup` + +Features: +- Color output (detect tty, ANSI fallback) +- `--env-file ` non-interactive mode: skip wizard, use provided .env, run docker + admin creation +- Ctrl+C trap for clean exit +- On docker failure: show `docker compose logs --tail 50` +- Works on Linux + macOS (bash, no bashisms beyond `read -s`) + +### 2b. Web Installer — `tools/installer/` (~4h, complexity: high) + +**Files**: +- `tools/installer/install-server.js` — zero-dependency Node.js HTTP server +- `tools/installer/install.html` — single-file SPA (inline CSS + JS) +- `tools/installer/README.md` + +**Server endpoints** (port 8090): +| Method | Path | Purpose | +|---|---|---| +| GET | `/` | Serve `install.html` | +| GET | `/api/defaults` | Env var catalog with classifications | +| POST | `/api/generate-secret` | `crypto.randomBytes(32).toString('hex')` | +| POST | `/api/save-env` | Write `.env` to project root | +| POST | `/api/start` | `docker compose up -d` | +| GET | `/api/status` | Container health polling | +| POST | `/api/create-admin` | Proxy to Oikos `/api/v1/auth/setup` | + +Server features: +- Node.js built-ins only (http, fs, child_process, crypto, path) +- Binds to `127.0.0.1:8090` +- Auto-terminates after successful admin creation (or 30min idle) +- CORS not needed (same origin) + +**UI design direction**: Clean, calm, trustworthy. Dark-mode-aware. Progress bar. Subtle step transitions. Google Fonts (loaded at runtime for installer only — installer is temporary, not part of the Docker image). + +Steps mirror CLI: config → secrets → integrations → review → docker start → admin creation → success. + +### 3. Documentation & Housekeeping (~30min, complexity: low) + +- Update `docs/installation.md` to reference both installer paths +- Add `tools/` to `.dockerignore` +- Add `install.sh` to `.dockerignore` + +## Commit Sequence + +1. `feat(api): add first-run setup endpoint for admin bootstrap` + - `server/auth.js`: new `/setup` route + - `server/index.js`: mount before auth guard + - `test-setup.js` + `package.json` test script +2. `feat(installer): add CLI install script` + - `install.sh` +3. `feat(installer): add web-based installer server and UI` + - `tools/installer/install-server.js` + - `tools/installer/install.html` + - `tools/installer/README.md` +4. `chore: add installer files to .dockerignore and update docs` + - `.dockerignore` additions + - `docs/installation.md` updates + +## Decisions (confirmed) + +1. **Fonts**: System font stack in web installer. No Google Fonts, no external dependencies. +2. **No Docker-exec fallback**: Installer targets current version with setup endpoint only. +3. **TRUST_PROXY**: Only via `.env`. Don't modify `docker-compose.yml`. diff --git a/docs/installer-recon.md b/docs/installer-recon.md new file mode 100644 index 0000000..377ff53 --- /dev/null +++ b/docs/installer-recon.md @@ -0,0 +1,97 @@ +# Installer Reconnaissance + +## 1. Environment Variables (from `.env.example`) + +### Auto-generatable (openssl rand -hex 32) +| Variable | Purpose | +|---|---| +| `SESSION_SECRET` | Express session signing key | +| `DB_ENCRYPTION_KEY` | SQLCipher encryption key | + +### Has sensible defaults +| Variable | Default | Notes | +|---|---|---| +| `PORT` | `3000` | | +| `NODE_ENV` | `production` | Hardcoded in docker-compose.yml | +| `DB_PATH` | `/data/oikos.db` | Hardcoded in docker-compose.yml | +| `SESSION_SECURE` | `true` | Set to `false` in docker-compose when no reverse proxy | +| `OPENWEATHER_CITY` | `Berlin` | | +| `OPENWEATHER_UNITS` | `metric` | | +| `OPENWEATHER_LANG` | `de` | | +| `APPLE_CALDAV_URL` | `https://caldav.icloud.com` | | +| `SYNC_INTERVAL_MINUTES` | `15` | | +| `RATE_LIMIT_WINDOW_MS` | `60000` | | +| `RATE_LIMIT_MAX_ATTEMPTS` | `5` | | +| `RATE_LIMIT_BLOCK_DURATION_MS` | `900000` | | + +### User-provided (optional integrations) +| Variable | Integration | +|---|---| +| `OPENWEATHER_API_KEY` | Weather widget | +| `GOOGLE_CLIENT_ID` | Google Calendar sync | +| `GOOGLE_CLIENT_SECRET` | Google Calendar sync | +| `GOOGLE_REDIRECT_URI` | Google Calendar sync | +| `APPLE_USERNAME` | Apple CalDAV sync | +| `APPLE_APP_SPECIFIC_PASSWORD` | Apple CalDAV sync | + +### Docker-compose overrides +These are set in `docker-compose.yml` `environment:` section and override `.env`: +- `NODE_ENV=production` +- `DB_PATH=/data/oikos.db` +- `SESSION_SECURE=false` (default, commented advice to remove for reverse proxy) + +## 2. Docker Setup + +- **Service name**: `oikos` +- **Image**: `ghcr.io/ulsklyc/oikos:latest` (or local build) +- **Port**: `0.0.0.0:3000:3000` +- **Volume**: `oikos_data:/data` (named volume) +- **Env file**: `.env` +- **Restart policy**: `unless-stopped` +- **Health check**: `GET http://localhost:3000/health` — interval 30s, timeout 10s, 3 retries, 10s start period + +## 3. Health Check Endpoint + +``` +GET /health → { status: "ok", timestamp: "2025-..." } +``` + +Returns HTTP 200 when the app is running and the DB is initialized. Excluded from rate limiting. + +## 4. Admin Creation — Current Mechanisms + +### a) `setup.js` (CLI) +- Interactive Node.js script, run via `npm run setup` (`node --import dotenv/config setup.js`) +- Directly accesses the SQLite DB via `server/db.js` +- Prompts: username, display_name, password (with confirmation) +- Checks if admin already exists, asks to confirm if so +- **Limitation**: Requires direct filesystem access to the DB — does NOT work when the app runs in Docker (DB is inside container volume at `/data/oikos.db`) + +### b) `POST /api/v1/auth/users` (API) +- Creates a new user +- **Requires**: Active admin session + CSRF token +- Fields: `{ username, display_name, password, avatar_color?, role? }` +- **Limitation**: Unusable for first-time setup (chicken-and-egg: need admin to create admin) + +### c) `scripts/seed-demo.js` +- Demo data seeding script — creates users directly via DB +- Not a setup mechanism, but shows the user schema + +## 5. Gap Analysis — What's Missing + +**A bootstrap API endpoint is needed.** Neither existing mechanism allows creating the first admin user when the app runs in Docker without shell access. + +**Proposed solution**: Add `POST /api/v1/auth/setup` endpoint: +- Only succeeds when the `users` table has zero rows +- No authentication required (it IS the authentication bootstrap) +- Accepts: `{ username, display_name, password }` +- Returns: `{ user: { id, username, display_name, role: 'admin' } }` +- After the first user exists, returns 403 ("Setup already completed") +- Rate-limited to prevent abuse during the brief window + +## 6. Existing Files to Be Aware Of + +- `.dockerignore` already excludes `docs/`, `scripts/`, `test-*.js`, `.env*` +- `tools/` is NOT in `.dockerignore` yet — needs to be added +- `docs/installation.md` exists — should be updated to reference the new installers +- `docs/install.html` exists — appears to be a landing page, not an installer