fix: normalize env var comparisons to be case-insensitive

All NODE_ENV, DEMO_MODE, OIDC_ONLY, FORCE_HTTPS, COOKIE_SECURE, and
ALLOW_INTERNAL_NETWORK checks now use .toLowerCase() so values like
'Production' or 'True' behave identically to their lowercase forms.
Also adds APP_VERSION to the startup banner.
This commit is contained in:
jubnl
2026-04-27 14:18:16 +02:00
parent 8e05ba7b20
commit 60808da238
16 changed files with 25 additions and 24 deletions
+3 -3
View File
@@ -53,7 +53,7 @@ export function createApp(): express.Application {
const app = express();
// Trust first proxy (nginx/Docker) for correct req.ip
if (process.env.NODE_ENV === 'production' || process.env.TRUST_PROXY) {
if (process.env.NODE_ENV?.toLowerCase() === 'production' || process.env.TRUST_PROXY) {
app.set('trust proxy', Number.parseInt(process.env.TRUST_PROXY) || 1);
}
@@ -67,13 +67,13 @@ export function createApp(): express.Application {
if (!origin || allowedOrigins.includes(origin)) callback(null, true);
else callback(new Error('Not allowed by CORS'));
};
} else if (process.env.NODE_ENV === 'production') {
} else if (process.env.NODE_ENV?.toLowerCase() === 'production') {
corsOrigin = false;
} else {
corsOrigin = true;
}
const shouldForceHttps = process.env.FORCE_HTTPS === 'true';
const shouldForceHttps = process.env.FORCE_HTTPS?.toLowerCase() === 'true';
// HSTS is worth enabling any time we're serving production traffic,
// not only when FORCE_HTTPS is set. Self-hosters behind Traefik /
// Caddy / Cloudflare Tunnel typically leave FORCE_HTTPS unset (the
+1 -1
View File
@@ -105,7 +105,7 @@ export const ENCRYPTION_KEY = _encryptionKey;
// Must stay in sync with client/src/i18n/supportedLanguages.ts (canonical source).
// Kept duplicated here because server and client are separate npm packages.
const SUPPORTED_LANG_CODES = ['de', 'en', 'es', 'fr', 'hu', 'nl', 'br', 'cs', 'pl', 'ru', 'zh', 'zh-TW', 'it', 'ar'];
const rawDefaultLang = process.env.DEFAULT_LANGUAGE || 'en';
const rawDefaultLang = process.env.DEFAULT_LANGUAGE?.toLowerCase() || 'en';
if (!SUPPORTED_LANG_CODES.includes(rawDefaultLang)) {
console.warn(`DEFAULT_LANGUAGE="${rawDefaultLang}" is not supported. Falling back to "en". Supported: ${SUPPORTED_LANG_CODES.join(', ')}`);
}
+1 -1
View File
@@ -47,7 +47,7 @@ const db = new Proxy({} as Database.Database, {
},
});
if (process.env.DEMO_MODE === 'true') {
if (process.env.DEMO_MODE?.toLowerCase() === 'true') {
try {
const { seedDemoData } = require('../demo/demo-seed');
seedDemoData(_db);
+1 -1
View File
@@ -6,7 +6,7 @@ import crypto from 'crypto';
// are only relevant after the first user exists; at that point seeds have already
// finished and skip via the userCount > 0 guard above.
function isOidcOnlyConfigured(): boolean {
if (process.env.OIDC_ONLY !== 'true') return false;
if (process.env.OIDC_ONLY?.toLowerCase() !== 'true') return false;
return !!(process.env.OIDC_ISSUER && process.env.OIDC_CLIENT_ID);
}
+4 -3
View File
@@ -29,8 +29,9 @@ const server = app.listen(PORT, () => {
const banner = [
'──────────────────────────────────────',
' TREK API started',
` Version ${process.env.APP_VERSION}`,
` Port: ${PORT}`,
` Environment: ${process.env.NODE_ENV || 'development'}`,
` Environment: ${process.env.NODE_ENV?.toLowerCase() || 'development'}`,
` Timezone: ${tz}`,
` Origins: ${origins}`,
` Log level: ${LOG_LVL}`,
@@ -40,8 +41,8 @@ const server = app.listen(PORT, () => {
'──────────────────────────────────────',
];
banner.forEach(l => console.log(l));
if (process.env.DEMO_MODE === 'true') sLogInfo('Demo mode: ENABLED');
if (process.env.DEMO_MODE === 'true' && process.env.NODE_ENV === 'production') {
if (process.env.DEMO_MODE?.toLowerCase() === 'true') sLogInfo('Demo mode: ENABLED');
if (process.env.DEMO_MODE?.toLowerCase() === 'true' && process.env.NODE_ENV?.toLowerCase() === 'production') {
sLogWarn('SECURITY WARNING: DEMO_MODE is enabled in production!');
}
scheduler.start();
+1 -1
View File
@@ -105,7 +105,7 @@ const adminOnly = (req: Request, res: Response, next: NextFunction): void => {
const demoUploadBlock = (req: Request, res: Response, next: NextFunction): void => {
const authReq = req as AuthRequest;
if (process.env.DEMO_MODE === 'true' && isDemoEmail(authReq.user?.email)) {
if (process.env.DEMO_MODE?.toLowerCase() === 'true' && isDemoEmail(authReq.user?.email)) {
res.status(403).json({ error: 'Uploads are disabled in demo mode. Self-host TREK for full functionality.' });
return;
}
+1 -1
View File
@@ -68,7 +68,7 @@ export function enforceGlobalMfaPolicy(req: Request, res: Response, next: NextFu
return;
}
if (process.env.DEMO_MODE === 'true' && verified.email && DEMO_EMAILS.has(verified.email)) {
if (process.env.DEMO_MODE?.toLowerCase() === 'true' && verified.email && DEMO_EMAILS.has(verified.email)) {
next();
return;
}
+1 -1
View File
@@ -449,7 +449,7 @@ router.put('/default-user-settings', (req: Request, res: Response) => {
});
// ── Dev-only: test notification endpoints ──────────────────────────────────────
if (process.env.NODE_ENV === 'development') {
if (process.env.NODE_ENV?.toLowerCase() === 'development') {
const { send } = require('../services/notificationService');
router.post('/dev/test-notification', async (req: Request, res: Response) => {
+1 -1
View File
@@ -168,7 +168,7 @@ router.put('/auto-settings', (req: Request, res: Response) => {
const msg = err instanceof Error ? err.message : String(err);
res.status(500).json({
error: 'Could not save auto-backup settings',
detail: process.env.NODE_ENV !== 'production' ? msg : undefined,
detail: process.env.NODE_ENV?.toLowerCase() !== 'production' ? msg : undefined,
});
}
});
+2 -2
View File
@@ -30,7 +30,7 @@ router.get('/login', async (req: Request, res: Response) => {
const config = getOidcConfig();
if (!config) return res.status(400).json({ error: 'OIDC not configured' });
if (config.issuer && !config.issuer.startsWith('https://') && process.env.NODE_ENV === 'production') {
if (config.issuer && !config.issuer.startsWith('https://') && process.env.NODE_ENV?.toLowerCase() === 'production') {
return res.status(400).json({ error: 'OIDC issuer must use HTTPS in production' });
}
@@ -85,7 +85,7 @@ router.get('/callback', async (req: Request, res: Response) => {
const config = getOidcConfig();
if (!config) return res.redirect(frontendUrl('/login?oidc_error=not_configured'));
if (config.issuer && !config.issuer.startsWith('https://') && process.env.NODE_ENV === 'production') {
if (config.issuer && !config.issuer.startsWith('https://') && process.env.NODE_ENV?.toLowerCase() === 'production') {
return res.redirect(frontendUrl('/login?oidc_error=issuer_not_https'));
}
+1 -1
View File
@@ -139,7 +139,7 @@ let demoTask: ScheduledTask | null = null;
function startDemoReset(): void {
if (demoTask) { demoTask.stop(); demoTask = null; }
if (process.env.DEMO_MODE !== 'true') return;
if (process.env.DEMO_MODE?.toLowerCase() !== 'true') return;
demoTask = cron.schedule('0 * * * *', () => {
try {
+1 -1
View File
@@ -288,7 +288,7 @@ export function updateOidcSettings(data: {
// ── Demo Baseline ──────────────────────────────────────────────────────────
export function saveDemoBaseline(): { error?: string; status?: number; message?: string } {
if (process.env.DEMO_MODE !== 'true') {
if (process.env.DEMO_MODE?.toLowerCase() !== 'true') {
return { error: 'Not found', status: 404 };
}
try {
+3 -3
View File
@@ -131,7 +131,7 @@ export function resolveAuthToggles(): {
oidc_login: get('oidc_login') !== 'false',
oidc_registration: get('oidc_registration') !== 'false',
};
if (process.env.OIDC_ONLY === 'true') {
if (process.env.OIDC_ONLY?.toLowerCase() === 'true') {
result.password_login = false;
result.password_registration = false;
}
@@ -139,7 +139,7 @@ export function resolveAuthToggles(): {
}
// Legacy fallback
const oidcOnlyEnabled = process.env.OIDC_ONLY === 'true' || get('oidc_only') === 'true';
const oidcOnlyEnabled = process.env.OIDC_ONLY?.toLowerCase() === 'true' || get('oidc_only') === 'true';
const oidcConfigured = !!(
(process.env.OIDC_ISSUER || get('oidc_issuer')) &&
(process.env.OIDC_CLIENT_ID || get('oidc_client_id'))
@@ -253,7 +253,7 @@ export function getPendingMfaSecret(userId: number): string | null {
export function getAppConfig(authenticatedUser: { id: number } | null) {
const userCount = (db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }).count;
const isDemo = process.env.DEMO_MODE === 'true';
const isDemo = process.env.DEMO_MODE?.toLowerCase() === 'true';
const toggles = resolveAuthToggles();
const version: string = process.env.APP_VERSION ?? require('../../package.json').version;
const hasGoogleKey = !!db.prepare("SELECT maps_api_key FROM users WHERE role = 'admin' AND maps_api_key IS NOT NULL AND maps_api_key != '' LIMIT 1").get();
+2 -2
View File
@@ -18,10 +18,10 @@ const COOKIE_NAME = 'trek_session';
* remains the explicit escape hatch for plain-HTTP LAN testing.
*/
export function cookieOptions(clear = false, req?: Request) {
if (process.env.COOKIE_SECURE === 'false') {
if (process.env.COOKIE_SECURE?.toLowerCase() === 'false') {
return buildOptions(clear, false);
}
const envSecure = process.env.NODE_ENV === 'production' || process.env.FORCE_HTTPS === 'true';
const envSecure = process.env.NODE_ENV?.toLowerCase() === 'production' || process.env.FORCE_HTTPS?.toLowerCase() === 'true';
const requestSecure = req?.secure === true;
return buildOptions(clear, envSecure || requestSecure);
}
+1 -1
View File
@@ -170,7 +170,7 @@ export async function send(payload: NotificationPayload): Promise<void> {
const configEntry = EVENT_NOTIFICATION_CONFIG[event];
if (!configEntry) {
logDebug(`notificationService.send: unknown event type "${event}", using fallback`);
if (process.env.NODE_ENV === 'development' && actorId != null) {
if (process.env.NODE_ENV?.toLowerCase() === 'development' && actorId != null) {
const devSender = (db.prepare('SELECT username, avatar FROM users WHERE id = ?').get(actorId) as { username: string; avatar: string | null } | undefined) ?? null;
createNotificationForRecipient({
type: 'simple',
+1 -1
View File
@@ -1,7 +1,7 @@
import dns from 'node:dns/promises';
import { Agent } from 'undici';
const ALLOW_INTERNAL_NETWORK = process.env.ALLOW_INTERNAL_NETWORK === 'true';
const ALLOW_INTERNAL_NETWORK = process.env.ALLOW_INTERNAL_NETWORK?.toLowerCase() === 'true';
export interface SsrfResult {
allowed: boolean;