refactor: move sessionSecret+passwordHash to env vars, seed data/ from defaults/

- SESSION_SECRET and PASSWORD_HASH moved out of config.json into env vars
- data.ts: AppConfig no longer holds secrets; loadConfig/loadData seed from
  defaults/ on first run if data/ files are missing
- auth.ts: requireAuth/authRequest read SESSION_SECRET from process.env directly
- login/+server.ts: reads PASSWORD_HASH and SESSION_SECRET from process.env
- defaults/config.json: ships with image (no secrets)
- defaults/units.json: ships with image as initial unit data
- package.json: add dotenv dep; start/serve load .env via -r dotenv/config
- Dockerfile: copy defaults/ into image; data/ is PVC-only
- .env.example: documents required env vars for local dev
- Remove k8s/ — managed externally
This commit is contained in:
Falkan
2026-03-19 23:54:37 -04:00
parent 5b89585505
commit 5480bb246c
11 changed files with 352 additions and 122 deletions

View File

@@ -1,17 +1,17 @@
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'fs';
import { randomBytes } from 'crypto';
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, copyFileSync } from 'fs';
import { resolve } from 'path';
import type { UnitsData } from '$lib/types';
// Path is relative to project root (where the server runs from)
const DATA_DIR = resolve('data');
const DEFAULTS_DIR = resolve('defaults');
const UNITS_PATH = resolve(DATA_DIR, 'units.json');
const CONFIG_PATH = resolve(DATA_DIR, 'config.json');
const DEFAULT_UNITS_PATH = resolve(DEFAULTS_DIR, 'units.json');
const DEFAULT_CONFIG_PATH = resolve(DEFAULTS_DIR, 'config.json');
export interface AppConfig {
adminPath: string;
passwordHash: string;
sessionSecret: string;
/** Label shown next to the YOLO mode toggle. Defaults to "YOLO mode". */
yoloLabel?: string;
/** Description shown inline after the label. Defaults to "(cross-group conversions)". */
@@ -24,10 +24,24 @@ export interface AppConfig {
yoloVisibility?: 'auto' | 'never';
}
/** Read units.json synchronously. Returns parsed UnitsData. */
/** Ensure data/ directory exists and seed missing files from defaults/. */
function ensureDataDir(): void {
mkdirSync(DATA_DIR, { recursive: true });
if (!existsSync(UNITS_PATH) && existsSync(DEFAULT_UNITS_PATH)) {
copyFileSync(DEFAULT_UNITS_PATH, UNITS_PATH);
console.log('[humor-units] First run: seeded data/units.json from defaults/units.json');
}
if (!existsSync(CONFIG_PATH) && existsSync(DEFAULT_CONFIG_PATH)) {
copyFileSync(DEFAULT_CONFIG_PATH, CONFIG_PATH);
console.log('[humor-units] First run: seeded data/config.json from defaults/config.json');
}
}
/** Read units.json synchronously. Seeds from defaults on first run. */
export function loadData(): UnitsData {
ensureDataDir();
if (!existsSync(UNITS_PATH)) {
throw new Error(`[humor-units] units.json not found at ${UNITS_PATH}. Mount it via a ConfigMap or PVC.`);
throw new Error(`[humor-units] units.json not found at ${UNITS_PATH} and no default available.`);
}
const raw = readFileSync(UNITS_PATH, 'utf8');
return JSON.parse(raw) as UnitsData;
@@ -40,19 +54,11 @@ export function saveData(data: UnitsData): void {
renameSync(tmp, UNITS_PATH);
}
/** Read config.json synchronously. Creates a default config on first run if missing. */
/** Read config.json synchronously. Seeds from defaults on first run. */
export function loadConfig(): AppConfig {
ensureDataDir();
if (!existsSync(CONFIG_PATH)) {
mkdirSync(DATA_DIR, { recursive: true });
const defaultConfig: AppConfig = {
adminPath: 'admin',
passwordHash: '',
sessionSecret: randomBytes(32).toString('hex'),
};
writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
console.log('[humor-units] First run: generated default config.json with random sessionSecret.');
console.log('[humor-units] Set an admin password via: node scripts/set-password.js <password>');
return defaultConfig;
throw new Error(`[humor-units] config.json not found at ${CONFIG_PATH} and no default available.`);
}
const raw = readFileSync(CONFIG_PATH, 'utf8');
return JSON.parse(raw) as AppConfig;