Files
temetro/backend/src/env.ts
T
Claude 3f270934f2 Fix Docker runtime: secure cookies over HTTP and empty env vars
- useSecureCookies now keys off the BETTER_AUTH_URL scheme instead of
  NODE_ENV, so login works over http://localhost in the (production-mode)
  Docker stack instead of the browser silently dropping the session cookie.
- env parsing treats empty strings as unset, so compose-supplied optionals
  like `SMTP_PORT=` no longer fail coercion (Number("") === 0) and crash boot.
- docker-compose: configurable host Postgres port (POSTGRES_PORT) to avoid
  clashing with an existing local Postgres.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:43:28 +03:00

56 lines
2.0 KiB
TypeScript

import "dotenv/config";
import { z } from "zod";
// Defaults keep this module loadable even with no .env present (e.g. when the
// Better Auth / drizzle-kit CLIs introspect the config offline). Real values
// come from .env at runtime; production misconfiguration is caught below.
const schema = z.object({
DATABASE_URL: z
.string()
.min(1)
.default("postgres://temetro:temetro@localhost:5432/temetro"),
BETTER_AUTH_SECRET: z.string().min(1).default("dev-insecure-secret-change-me"),
BETTER_AUTH_URL: z.string().min(1).default("http://localhost:4000"),
FRONTEND_URL: z.string().min(1).default("http://localhost:3000"),
PORT: z.coerce.number().int().positive().default(4000),
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().int().positive().optional(),
SMTP_USER: z.string().optional(),
SMTP_PASS: z.string().optional(),
SMTP_FROM: z.string().default("temetro <no-reply@temetro.local>"),
});
// docker compose passes unset optionals as empty strings (e.g. `${SMTP_PORT:-}`).
// Treat empty strings as "unset" so optionals/defaults apply instead of failing
// coercion (e.g. Number("") === 0).
const rawEnv = Object.fromEntries(
Object.entries(process.env).map(([k, v]) => [k, v === "" ? undefined : v]),
);
const parsed = schema.safeParse(rawEnv);
if (!parsed.success) {
const lines = parsed.error.issues
.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`)
.join("\n");
console.error(`❌ Invalid environment variables:\n${lines}`);
process.exit(1);
}
export const env = parsed.data;
// Fail fast on dangerous production misconfiguration.
if (env.NODE_ENV === "production") {
if (env.BETTER_AUTH_SECRET === "dev-insecure-secret-change-me") {
console.error(
"❌ BETTER_AUTH_SECRET is unset in production. Generate one: openssl rand -base64 32",
);
process.exit(1);
}
}
export const isProd = env.NODE_ENV === "production";