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
This commit is contained in:
2026-06-20 07:18:47 +00:00
parent f133d11123
commit e335e257a2
3 changed files with 49 additions and 6 deletions
+41
View File
@@ -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 <access_token>"
```
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
+1 -1
View File
@@ -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;'
+7 -5
View File
@@ -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>('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<void> => {
return lastValueFrom(http.get<AppConfig>('./config.json')).then(
(config) => {
Object.assign(_appConfig, config);
},
return lastValueFrom(http.get<AppConfig>('config.json')).then(
(config) => { Object.assign(_appConfig, config); },
() => {
// Keep defaults if config.json is not available (dev mode).
},