import 'dotenv/config'; import path from 'node:path'; import crypto from 'node:crypto'; function bool(value, fallback) { if (value === undefined || value === '') return fallback; return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase()); } function int(value, fallback) { const n = Number.parseInt(value, 10); return Number.isFinite(n) ? n : fallback; } const dataDir = process.env.DATA_DIR || '/data'; if (!process.env.APP_SECRET) { console.warn( '[config] APP_SECRET is not set. A random one is being generated for this process only.\n' + ' Sessions will drop and stored visitor PINs will become unreadable on restart.\n' + ' Set APP_SECRET in your .env before going live.' ); } export const config = { port: int(process.env.PORT, 3000), siteName: process.env.SITE_NAME || 'Visitor sign in', timezone: process.env.TZ || 'Australia/Melbourne', dataDir, dbPath: process.env.DB_PATH || path.join(dataDir, 'visitors.db'), photoDir: process.env.PHOTO_DIR || path.join(dataDir, 'photos'), appSecret: process.env.APP_SECRET || crypto.randomBytes(32).toString('hex'), trustProxy: bool(process.env.TRUST_PROXY, false), secureCookies: bool(process.env.SECURE_COOKIES, false), admin: { // Used once, to create the first account if the user table is empty. bootstrapEmail: (process.env.ADMIN_BOOTSTRAP_EMAIL || '').trim().toLowerCase(), bootstrapPassword: process.env.ADMIN_BOOTSTRAP_PASSWORD || process.env.ADMIN_PASSWORD || '', // Blank allows any address. Otherwise a comma separated list, e.g. "school.vic.edu.au". allowedDomains: (process.env.ADMIN_ALLOWED_DOMAINS || '') .split(',') .map((d) => d.trim().toLowerCase().replace(/^@/, '')) .filter(Boolean), require2fa: bool(process.env.ADMIN_REQUIRE_2FA, true), }, printing: { // brother_ql drives the label printer over the network. Overridable so a // wrapper or a different binary can be swapped in. command: process.env.PRINT_COMMAND || 'brother_ql', timeoutMs: int(process.env.PRINT_TIMEOUT_MS, 15000), // How long a sign in waits for the badge before falling back to the browser. signInWaitMs: int(process.env.PRINT_SIGNIN_WAIT_MS, 9000), }, // Admins are warned this many days before a WWCC or VIT expires. expiryWarningDays: int(process.env.EXPIRY_WARNING_DAYS, 28), requirePhoto: bool(process.env.REQUIRE_PHOTO, true), photoRetentionDays: int(process.env.PHOTO_RETENTION_DAYS, 90), // Blank disables the nightly sweep. Format "HH:MM" in local time. autoSignOutTime: process.env.AUTO_SIGNOUT_TIME || '', https: { enabled: bool(process.env.HTTPS_ENABLED, false), keyPath: process.env.HTTPS_KEY || path.join(dataDir, 'certs', 'server.key'), certPath: process.env.HTTPS_CERT || path.join(dataDir, 'certs', 'server.crt'), // Names and addresses staff will actually type. Baked into the certificate. hostnames: (process.env.HTTPS_HOSTNAMES || 'visitors.local') .split(',') .map((h) => h.trim()) .filter(Boolean), // A plain http listener that serves the CA certificate and redirects // everything else to https. 0 turns it off. redirectPort: int(process.env.HTTP_REDIRECT_PORT, 3001), // The https port as published on the docker host, used when redirecting. publicPort: int(process.env.HTTPS_PUBLIC_PORT, 8443), }, sheets: { enabled: bool(process.env.SHEETS_ENABLED, false), spreadsheetId: process.env.SHEETS_SPREADSHEET_ID || '', // Append-only history of every sign in and sign out. logTab: process.env.SHEETS_LOG_TAB || process.env.SHEETS_TAB_NAME || 'Visitor log', // Rewritten on every change: just the people currently on site. onSiteTab: process.env.SHEETS_ONSITE_TAB || 'On site now', // Either a path to the service account JSON, or the JSON itself base64 encoded. credentialsPath: process.env.GOOGLE_CREDENTIALS_PATH || '', credentialsB64: process.env.GOOGLE_CREDENTIALS_B64 || '', retryIntervalMs: int(process.env.SHEETS_RETRY_INTERVAL_MS, 60000), }, }; export default config;