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:
@@ -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
|
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
|
## Configuration
|
||||||
|
|
||||||
| Variable | Default | Where | Description |
|
| 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_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_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_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_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 |
|
| `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_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain
|
||||||
KMTN_CORS_ORIGINS: '["http://yourdomain.com"]'
|
KMTN_CORS_ORIGINS: '["http://yourdomain.com"]'
|
||||||
KMTN_ENV: production
|
KMTN_ENV: production
|
||||||
|
KMTN_ADMIN_USERNAME: admin
|
||||||
|
KMTN_ADMIN_PASSWORD: change-me
|
||||||
|
KMTN_JWT_SECRET_KEY: change-me-to-a-random-string
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -294,6 +327,9 @@ services:
|
|||||||
KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain
|
KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain
|
||||||
KMTN_CORS_ORIGINS: '["http://truenas.local:34200", "http://truenas.local"]'
|
KMTN_CORS_ORIGINS: '["http://truenas.local:34200", "http://truenas.local"]'
|
||||||
KMTN_ENV: production
|
KMTN_ENV: production
|
||||||
|
KMTN_ADMIN_USERNAME: admin
|
||||||
|
KMTN_ADMIN_PASSWORD: change-me
|
||||||
|
KMTN_JWT_SECRET_KEY: change-me-to-a-random-string
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -331,6 +367,9 @@ ng build --configuration production
|
|||||||
export KMTN_DATABASE_URL="postgresql+asyncpg://user:pass@host:5432/kmountain"
|
export KMTN_DATABASE_URL="postgresql+asyncpg://user:pass@host:5432/kmountain"
|
||||||
export KMTN_CORS_ORIGINS='["https://yourdomain.com"]'
|
export KMTN_CORS_ORIGINS='["https://yourdomain.com"]'
|
||||||
export KMTN_ENV="production"
|
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
|
# 3. Start the backend
|
||||||
cd backend
|
cd backend
|
||||||
@@ -348,6 +387,8 @@ The database auto-seeds on first startup.
|
|||||||
- [ ] Use a managed PostgreSQL database (RDS, Cloud SQL, etc.)
|
- [ ] Use a managed PostgreSQL database (RDS, Cloud SQL, etc.)
|
||||||
- [ ] Update `KMTN_CORS_ORIGINS` to your actual domain (not `localhost`)
|
- [ ] Update `KMTN_CORS_ORIGINS` to your actual domain (not `localhost`)
|
||||||
- [ ] Set `KMTN_ENV="production"` to disable admin endpoints
|
- [ ] 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
|
- [ ] Add a reverse proxy (Caddy, Traefik) in front of Nginx for HTTPS
|
||||||
- [ ] Set up log rotation
|
- [ ] Set up log rotation
|
||||||
- [ ] Configure a process manager (systemd, PM2) for the backend
|
- [ ] Configure a process manager (systemd, PM2) for the backend
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
envsubst '${API_UPSTREAM}' < /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/conf.d/default.conf
|
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;'
|
exec nginx -g 'daemon off;'
|
||||||
|
|||||||
@@ -2,22 +2,24 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { APP_INITIALIZER, InjectionToken } from '@angular/core';
|
import { APP_INITIALIZER, InjectionToken } from '@angular/core';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const APP_CONFIG = new InjectionToken<AppConfig>('appConfig');
|
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 = {
|
const _appConfig: AppConfig = {
|
||||||
apiBaseUrl: '',
|
apiBaseUrl: environment.apiBaseUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function appConfigFactory(http: HttpClient) {
|
export function appConfigFactory(http: HttpClient) {
|
||||||
return (): Promise<void> => {
|
return (): Promise<void> => {
|
||||||
return lastValueFrom(http.get<AppConfig>('./config.json')).then(
|
return lastValueFrom(http.get<AppConfig>('config.json')).then(
|
||||||
(config) => {
|
(config) => { Object.assign(_appConfig, config); },
|
||||||
Object.assign(_appConfig, config);
|
|
||||||
},
|
|
||||||
() => {
|
() => {
|
||||||
// Keep defaults if config.json is not available (dev mode).
|
// Keep defaults if config.json is not available (dev mode).
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user