Files
family_wishlist/apps/backend/src/config/env.ts

90 lines
2.8 KiB
TypeScript

import { z } from 'zod';
import crypto from 'node:crypto';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
BACKEND_PORT: z.coerce.number().int().positive().default(3000),
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
DATABASE_URL: z.string().url(),
PUBLIC_APP_URL: z.string().url().default('http://localhost:8080'),
UPLOADS_DIR: z.string().default('./uploads'),
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 chars'),
COOKIE_SECRET: z.string().min(32, 'COOKIE_SECRET must be at least 32 chars'),
USER1_USERNAME: z.string().min(3).max(64),
USER1_PASSWORD_HASH: z.string().min(20, 'USER1_PASSWORD_HASH must be a bcrypt hash'),
USER1_SLUG: z.string().min(3).max(32),
USER1_DISPLAY_NAME: z.string().min(1).max(64),
USER2_USERNAME: z.string().min(3).max(64),
USER2_PASSWORD_HASH: z.string().min(20, 'USER2_PASSWORD_HASH must be a bcrypt hash'),
USER2_SLUG: z.string().min(3).max(32),
USER2_DISPLAY_NAME: z.string().min(1).max(64),
});
export type Env = z.infer<typeof envSchema>;
function parseEnv(): Env {
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
// eslint-disable-next-line no-console
console.error('\nInvalid environment configuration:\n');
for (const issue of parsed.error.issues) {
// eslint-disable-next-line no-console
console.error(` - ${issue.path.join('.')}: ${issue.message}`);
}
process.exit(1);
}
return parsed.data;
}
export const env = parseEnv();
export interface EnvUserConfig {
id: string;
username: string;
passwordHash: string;
slug: string;
displayName: string;
}
function stableUserId(username: string): string {
// 24-char stable id derived from username so DB seed can upsert deterministically
// without depending on any external secret.
return 'u_' + crypto.createHash('sha256').update(`user:${username}`).digest('hex').slice(0, 22);
}
export function resolveUsers(): EnvUserConfig[] {
const usernames = new Set<string>();
const slugs = new Set<string>();
const users: EnvUserConfig[] = [
{
id: stableUserId(env.USER1_USERNAME),
username: env.USER1_USERNAME,
passwordHash: env.USER1_PASSWORD_HASH,
slug: env.USER1_SLUG,
displayName: env.USER1_DISPLAY_NAME,
},
{
id: stableUserId(env.USER2_USERNAME),
username: env.USER2_USERNAME,
passwordHash: env.USER2_PASSWORD_HASH,
slug: env.USER2_SLUG,
displayName: env.USER2_DISPLAY_NAME,
},
];
for (const u of users) {
if (usernames.has(u.username)) {
throw new Error(`Duplicate USER*_USERNAME: ${u.username}`);
}
if (slugs.has(u.slug)) {
throw new Error(`Duplicate USER*_SLUG: ${u.slug}`);
}
usernames.add(u.username);
slugs.add(u.slug);
}
return users;
}