feat: email verification + delete confirmation + smtp/cors/prod hardening
This commit is contained in:
@@ -14,6 +14,7 @@ const Env = z.object({
|
||||
HOST: z.string().default("0.0.0.0"),
|
||||
DATABASE_URL: z.string().min(1),
|
||||
CORS_ORIGIN: z.string().optional(),
|
||||
CORS_ORIGINS: z.string().optional(),
|
||||
RATE_LIMIT_MAX: z.coerce.number().int().positive().default(200),
|
||||
RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(60_000),
|
||||
JWT_SECRET: z.string().min(32),
|
||||
@@ -22,6 +23,22 @@ const Env = z.object({
|
||||
AUTH_DISABLED: BoolFromEnv.optional().default(false),
|
||||
SEED_DEFAULT_BUDGET: BoolFromEnv.default(true),
|
||||
SESSION_TIMEOUT_MINUTES: z.coerce.number().int().positive().default(30),
|
||||
APP_ORIGIN: z.string().min(1).default("http://localhost:5173"),
|
||||
UPDATE_NOTICE_VERSION: z.coerce.number().int().nonnegative().default(0),
|
||||
UPDATE_NOTICE_TITLE: z.string().min(1).default("SkyMoney Updated"),
|
||||
UPDATE_NOTICE_BODY: z
|
||||
.string()
|
||||
.min(1)
|
||||
.default("We shipped improvements and fixes. Please review the latest changes."),
|
||||
SMTP_HOST: z.string().optional(),
|
||||
SMTP_PORT: z.coerce.number().int().positive().optional(),
|
||||
SMTP_REQUIRE_TLS: BoolFromEnv.default(true),
|
||||
SMTP_TLS_REJECT_UNAUTHORIZED: BoolFromEnv.default(true),
|
||||
SMTP_USER: z.string().optional(),
|
||||
SMTP_PASS: z.string().optional(),
|
||||
EMAIL_FROM: z.string().min(1).default("SkyMoney <noreply@skymoneybudget.com>"),
|
||||
EMAIL_BOUNCE_FROM: z.string().optional(),
|
||||
EMAIL_REPLY_TO: z.string().optional(),
|
||||
});
|
||||
|
||||
const rawEnv = {
|
||||
@@ -30,6 +47,7 @@ const rawEnv = {
|
||||
HOST: process.env.HOST,
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
CORS_ORIGIN: process.env.CORS_ORIGIN,
|
||||
CORS_ORIGINS: process.env.CORS_ORIGINS,
|
||||
RATE_LIMIT_MAX: process.env.RATE_LIMIT_MAX,
|
||||
RATE_LIMIT_WINDOW_MS: process.env.RATE_LIMIT_WINDOW_MS,
|
||||
JWT_SECRET: process.env.JWT_SECRET ?? "dev-jwt-secret-change-me",
|
||||
@@ -37,13 +55,27 @@ const rawEnv = {
|
||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||
AUTH_DISABLED: process.env.AUTH_DISABLED,
|
||||
SEED_DEFAULT_BUDGET: process.env.SEED_DEFAULT_BUDGET,
|
||||
SESSION_TIMEOUT_MINUTES: process.env.SESSION_TIMEOUT_MINUTES,
|
||||
APP_ORIGIN: process.env.APP_ORIGIN,
|
||||
UPDATE_NOTICE_VERSION: process.env.UPDATE_NOTICE_VERSION,
|
||||
UPDATE_NOTICE_TITLE: process.env.UPDATE_NOTICE_TITLE,
|
||||
UPDATE_NOTICE_BODY: process.env.UPDATE_NOTICE_BODY,
|
||||
SMTP_HOST: process.env.SMTP_HOST,
|
||||
SMTP_PORT: process.env.SMTP_PORT,
|
||||
SMTP_REQUIRE_TLS: process.env.SMTP_REQUIRE_TLS,
|
||||
SMTP_TLS_REJECT_UNAUTHORIZED: process.env.SMTP_TLS_REJECT_UNAUTHORIZED,
|
||||
SMTP_USER: process.env.SMTP_USER,
|
||||
SMTP_PASS: process.env.SMTP_PASS,
|
||||
EMAIL_FROM: process.env.EMAIL_FROM,
|
||||
EMAIL_BOUNCE_FROM: process.env.EMAIL_BOUNCE_FROM,
|
||||
EMAIL_REPLY_TO: process.env.EMAIL_REPLY_TO,
|
||||
};
|
||||
|
||||
const parsed = Env.parse(rawEnv);
|
||||
|
||||
if (parsed.NODE_ENV === "production") {
|
||||
if (!parsed.CORS_ORIGIN) {
|
||||
throw new Error("CORS_ORIGIN must be set in production.");
|
||||
if (!parsed.CORS_ORIGIN && !parsed.CORS_ORIGINS) {
|
||||
throw new Error("CORS_ORIGIN or CORS_ORIGINS must be set in production.");
|
||||
}
|
||||
if (rawEnv.AUTH_DISABLED && parsed.AUTH_DISABLED) {
|
||||
throw new Error("AUTH_DISABLED cannot be enabled in production.");
|
||||
@@ -57,6 +89,9 @@ if (parsed.NODE_ENV === "production") {
|
||||
if (parsed.COOKIE_SECRET.includes("dev-cookie-secret-change-me")) {
|
||||
throw new Error("COOKIE_SECRET must be set to a strong value in production.");
|
||||
}
|
||||
if (!parsed.APP_ORIGIN) {
|
||||
throw new Error("APP_ORIGIN must be set in production.");
|
||||
}
|
||||
}
|
||||
|
||||
export const env = parsed;
|
||||
|
||||
Reference in New Issue
Block a user