generated from bastian/boilerplate
158 lines
5.0 KiB
TypeScript
158 lines
5.0 KiB
TypeScript
import { config as loadDotenv } from 'dotenv';
|
|
import { z } from 'zod';
|
|
import type { AppConfig } from './config.types';
|
|
|
|
export const envFilePaths = ['.env', '../../.env'];
|
|
|
|
const booleanFromString = z
|
|
.string()
|
|
.transform((value) => value.toLowerCase())
|
|
.pipe(z.enum(['true', 'false']))
|
|
.transform((value) => value === 'true');
|
|
|
|
const envSchema = z.object({
|
|
NODE_ENV: z
|
|
.enum(['development', 'test', 'production'])
|
|
.default('development'),
|
|
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
|
|
APP_BASE_URL: z.url(),
|
|
FRONTEND_BASE_URL: z.url().optional(),
|
|
TRUST_PROXY: booleanFromString.default(false),
|
|
DATABASE_HOST: z.string().min(1),
|
|
DATABASE_PORT: z.coerce.number().int().min(1).max(65535).default(3306),
|
|
DATABASE_NAME: z.string().min(1),
|
|
DATABASE_USER: z.string().min(1),
|
|
DATABASE_PASSWORD: z.string().min(1),
|
|
DATABASE_SSL: booleanFromString.default(false),
|
|
OIDC_ISSUER: z.url(),
|
|
OIDC_CLIENT_ID: z.string().min(1),
|
|
OIDC_CLIENT_SECRET: z.string().min(1),
|
|
OIDC_SCOPES: z.string().min(1).default('openid profile email'),
|
|
OIDC_LOGOUT_URL: z.preprocess(
|
|
(value) => (value === '' ? undefined : value),
|
|
z.url().optional(),
|
|
),
|
|
OIDC_ALLOWED_ALGORITHMS: z
|
|
.string()
|
|
.min(1)
|
|
.transform((value) =>
|
|
value
|
|
.split(/[\s,]+/)
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean),
|
|
)
|
|
.refine(
|
|
(algorithms) =>
|
|
algorithms.length > 0 &&
|
|
algorithms.every((algorithm) => algorithm.toLowerCase() !== 'none'),
|
|
'OIDC_ALLOWED_ALGORITHMS darf none nicht erlauben.',
|
|
),
|
|
OIDC_HTTP_TIMEOUT_MS: z.coerce
|
|
.number()
|
|
.int()
|
|
.min(1000)
|
|
.max(30000)
|
|
.default(5000),
|
|
SESSION_COOKIE_NAME: z.string().min(1).default('app_session'),
|
|
SESSION_IDLE_TIMEOUT_SECONDS: z.coerce.number().int().min(300).default(28800),
|
|
SESSION_ABSOLUTE_TIMEOUT_SECONDS: z.coerce
|
|
.number()
|
|
.int()
|
|
.min(3600)
|
|
.default(604800),
|
|
SESSION_SECRET: z.string().min(32),
|
|
SESSION_ENCRYPTION_KEY: z.string().min(32),
|
|
CORS_ORIGINS: z.string().transform((value) =>
|
|
value
|
|
.split(',')
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean),
|
|
),
|
|
CSRF_HEADER_NAME: z.string().min(1).default('X-CSRF-Token'),
|
|
LOG_LEVEL: z.string().min(1).default('info'),
|
|
SWAGGER_ENABLED: booleanFromString.default(false),
|
|
RATE_LIMIT_WINDOW_SECONDS: z.coerce.number().int().min(1).default(60),
|
|
RATE_LIMIT_MAX_REQUESTS: z.coerce.number().int().min(1).default(300),
|
|
RATE_LIMIT_SENSITIVE_WINDOW_SECONDS: z.coerce
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.default(60),
|
|
RATE_LIMIT_SENSITIVE_MAX_REQUESTS: z.coerce.number().int().min(1).default(10),
|
|
});
|
|
|
|
export function loadConfigFromEnv(env: Record<string, unknown>): AppConfig {
|
|
const parsed = envSchema.safeParse(env);
|
|
if (!parsed.success) {
|
|
const issues = parsed.error.issues.map(
|
|
(issue) => `${issue.path.join('.')}: ${issue.message}`,
|
|
);
|
|
throw new Error(`Ungueltige Konfiguration:\n${issues.join('\n')}`);
|
|
}
|
|
|
|
const value = parsed.data;
|
|
if (value.NODE_ENV === 'production') {
|
|
const insecure = [
|
|
value.SESSION_SECRET.includes('change'),
|
|
value.SESSION_ENCRYPTION_KEY.includes('change'),
|
|
value.OIDC_CLIENT_SECRET.includes('change'),
|
|
];
|
|
if (insecure.some(Boolean)) {
|
|
throw new Error(
|
|
'Production-Secrets muessen explizit und sicher gesetzt werden.',
|
|
);
|
|
}
|
|
}
|
|
|
|
return {
|
|
nodeEnv: value.NODE_ENV,
|
|
port: value.PORT,
|
|
appBaseUrl: value.APP_BASE_URL,
|
|
frontendBaseUrl: value.FRONTEND_BASE_URL ?? value.APP_BASE_URL,
|
|
trustProxy: value.TRUST_PROXY,
|
|
database: {
|
|
host: value.DATABASE_HOST,
|
|
port: value.DATABASE_PORT,
|
|
name: value.DATABASE_NAME,
|
|
user: value.DATABASE_USER,
|
|
password: value.DATABASE_PASSWORD,
|
|
ssl: value.DATABASE_SSL,
|
|
},
|
|
oidc: {
|
|
issuer: value.OIDC_ISSUER,
|
|
clientId: value.OIDC_CLIENT_ID,
|
|
clientSecret: value.OIDC_CLIENT_SECRET,
|
|
scopes: value.OIDC_SCOPES,
|
|
allowedAlgorithms: value.OIDC_ALLOWED_ALGORITHMS,
|
|
httpTimeoutMs: value.OIDC_HTTP_TIMEOUT_MS,
|
|
...(value.OIDC_LOGOUT_URL ? { logoutUrl: value.OIDC_LOGOUT_URL } : {}),
|
|
},
|
|
session: {
|
|
cookieName: value.SESSION_COOKIE_NAME,
|
|
idleTimeoutSeconds: value.SESSION_IDLE_TIMEOUT_SECONDS,
|
|
absoluteTimeoutSeconds: value.SESSION_ABSOLUTE_TIMEOUT_SECONDS,
|
|
secret: value.SESSION_SECRET,
|
|
encryptionKey: value.SESSION_ENCRYPTION_KEY,
|
|
},
|
|
corsOrigins: value.CORS_ORIGINS,
|
|
csrfHeaderName: value.CSRF_HEADER_NAME,
|
|
logLevel: value.LOG_LEVEL,
|
|
swaggerEnabled: value.SWAGGER_ENABLED,
|
|
rateLimit: {
|
|
global: {
|
|
windowSeconds: value.RATE_LIMIT_WINDOW_SECONDS,
|
|
maxRequests: value.RATE_LIMIT_MAX_REQUESTS,
|
|
},
|
|
sensitive: {
|
|
windowSeconds: value.RATE_LIMIT_SENSITIVE_WINDOW_SECONDS,
|
|
maxRequests: value.RATE_LIMIT_SENSITIVE_MAX_REQUESTS,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function loadConfigForCli(): AppConfig {
|
|
loadDotenv({ path: envFilePaths, override: false, quiet: true });
|
|
return loadConfigFromEnv(process.env);
|
|
}
|