Initial commit

This commit is contained in:
2026-07-19 13:09:04 +02:00
commit 8cf57d7878
215 changed files with 30417 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
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);
}