From e335e257a2f5788804dce587a7babc694da2b257 Mon Sep 17 00:00:00 2001 From: kfj001 Date: Sat, 20 Jun 2026 07:18:47 +0000 Subject: [PATCH] feat: add bootstrap admin auth docs and fix config loading - Document bootstrap admin login via KMTN_ADMIN_USERNAME/PASSWORD env vars, including setup guide, curl examples, and config table entries across docker-compose and bare-metal deployment sections - Add KMTN_JWT_SECRET_KEY to all deployment config examples and production checklist - Fix docker-entrypoint.sh: remove unused API_UPSTREAM variable from config.json envsubst (only API_PUBLIC_URL is needed) - Fix app-config.service: default apiBaseUrl now reads from Angular environment instead of empty string, and config.json path drops leading ./ for cleaner resolution --- README.md | 41 ++++++++++++++++++++++++++ docker-entrypoint.sh | 2 +- src/app/services/app-config.service.ts | 12 ++++---- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a31aff2..4dc4350 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,33 @@ The database auto-seeds on first startup. To reset and re-seed at any time: curl -X POST http://localhost:8000/api/admin/reset ``` +### Setting up the bootstrap admin user + +The backend supports a hardcoded "bootstrap admin" login via two environment variables. When both are set, anyone with those credentials can authenticate at `POST /api/auth/login` and receives a JWT token with admin privileges. + +```bash +# Enable bootstrap admin login +export KMTN_ADMIN_USERNAME="admin" +export KMTN_ADMIN_PASSWORD="my-secret-password" +``` + +To log in, send a POST request to `/api/auth/login`: + +```bash +curl -X POST http://localhost:8000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username": "admin", "password": "my-secret-password"}' +``` + +This returns a JWT access token you can use to access protected endpoints: + +```bash +curl http://localhost:8000/api/auth/me \ + -H "Authorization: Bearer " +``` + +If either variable is left empty (the default), login is disabled and returns a 503 error. For production, also set `KMTN_JWT_SECRET_KEY` to a strong random value. + ## Configuration | Variable | Default | Where | Description | @@ -145,6 +172,9 @@ curl -X POST http://localhost:8000/api/admin/reset | `KMTN_DATABASE_URL` | `postgresql+asyncpg://...` (prod) / `sqlite+aiosqlite:///./kmountain.db` (dev) | [backend/app/config.py](backend/app/config.py) | Database connection string | | `KMTN_CORS_ORIGINS` | `["http://localhost:4200"]` | [backend/app/config.py](backend/app/config.py) | CORS allowlist (JSON array) | | `KMTN_ENV` | `development` | [backend/app/config.py](backend/app/config.py) | Set to `production` to disable admin endpoints | +| `KMTN_ADMIN_USERNAME` | _(empty)_ | [backend/app/config.py](backend/app/config.py) | Bootstrap admin username. Leave empty to disable login. | +| `KMTN_ADMIN_PASSWORD` | _(empty)_ | [backend/app/config.py](backend/app/config.py) | Bootstrap admin password. Both username and password must be set to enable login. | +| `KMTN_JWT_SECRET_KEY` | `change-me-in-production` | [backend/app/config.py](backend/app/config.py) | Secret key used to sign and verify JWT tokens. If leaked, anyone can forge admin tokens. **Change in production.** | | `API_UPSTREAM` | `http://api:8000` | [Dockerfile](Dockerfile) | Internal API target for Nginx proxy (Docker DNS) | | `API_PUBLIC_URL` | `""` (empty) | [Dockerfile](Dockerfile) | Browser-facing API URL. Empty = proxy mode; set to direct URL when ingress breaks proxy chain | @@ -235,6 +265,9 @@ services: KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain KMTN_CORS_ORIGINS: '["http://yourdomain.com"]' KMTN_ENV: production + KMTN_ADMIN_USERNAME: admin + KMTN_ADMIN_PASSWORD: change-me + KMTN_JWT_SECRET_KEY: change-me-to-a-random-string depends_on: db: condition: service_healthy @@ -294,6 +327,9 @@ services: KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain KMTN_CORS_ORIGINS: '["http://truenas.local:34200", "http://truenas.local"]' KMTN_ENV: production + KMTN_ADMIN_USERNAME: admin + KMTN_ADMIN_PASSWORD: change-me + KMTN_JWT_SECRET_KEY: change-me-to-a-random-string ports: - "8000:8000" depends_on: @@ -331,6 +367,9 @@ ng build --configuration production export KMTN_DATABASE_URL="postgresql+asyncpg://user:pass@host:5432/kmountain" export KMTN_CORS_ORIGINS='["https://yourdomain.com"]' export KMTN_ENV="production" +export KMTN_ADMIN_USERNAME="admin" +export KMTN_ADMIN_PASSWORD="change-me" +export KMTN_JWT_SECRET_KEY="change-me-to-a-random-string" # 3. Start the backend cd backend @@ -348,6 +387,8 @@ The database auto-seeds on first startup. - [ ] Use a managed PostgreSQL database (RDS, Cloud SQL, etc.) - [ ] Update `KMTN_CORS_ORIGINS` to your actual domain (not `localhost`) - [ ] Set `KMTN_ENV="production"` to disable admin endpoints +- [ ] Set `KMTN_ADMIN_USERNAME` and `KMTN_ADMIN_PASSWORD` to strong values +- [ ] Set `KMTN_JWT_SECRET_KEY` to a strong random value - [ ] Add a reverse proxy (Caddy, Traefik) in front of Nginx for HTTPS - [ ] Set up log rotation - [ ] Configure a process manager (systemd, PM2) for the backend diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 8eebcac..df72a93 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -3,6 +3,6 @@ set -e envsubst '${API_UPSTREAM}' < /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/conf.d/default.conf -envsubst '${API_UPSTREAM},${API_PUBLIC_URL}' < /usr/share/nginx/html/config.json.template > /usr/share/nginx/html/config.json +envsubst '${API_PUBLIC_URL}' < /usr/share/nginx/html/config.json.template > /usr/share/nginx/html/config.json exec nginx -g 'daemon off;' diff --git a/src/app/services/app-config.service.ts b/src/app/services/app-config.service.ts index 703d090..20faaf0 100644 --- a/src/app/services/app-config.service.ts +++ b/src/app/services/app-config.service.ts @@ -2,22 +2,24 @@ import { HttpClient } from '@angular/common/http'; import { APP_INITIALIZER, InjectionToken } from '@angular/core'; import { lastValueFrom } from 'rxjs'; +import { environment } from '../../environments/environment'; + export interface AppConfig { apiBaseUrl: string; } export const APP_CONFIG = new InjectionToken('appConfig'); +// Default to the Angular environment value (http://localhost:8000 in dev, '' in prod). +// A runtime config.json can override this in production. const _appConfig: AppConfig = { - apiBaseUrl: '', + apiBaseUrl: environment.apiBaseUrl, }; export function appConfigFactory(http: HttpClient) { return (): Promise => { - return lastValueFrom(http.get('./config.json')).then( - (config) => { - Object.assign(_appConfig, config); - }, + return lastValueFrom(http.get('config.json')).then( + (config) => { Object.assign(_appConfig, config); }, () => { // Keep defaults if config.json is not available (dev mode). },