docs(installer): add reconnaissance findings and implementation plan
This commit is contained in:
@@ -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 <path>` 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`.
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user