generated from bastian/boilerplate
Initial commit
This commit is contained in:
157
apps/backend/src/config/env.ts
Normal file
157
apps/backend/src/config/env.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user