90 lines
2.8 KiB
TypeScript
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;
|
|
}
|