23bd0f0c6a
Every cookie was flagged Secure whenever NODE_ENV=production. Over plain HTTP (single-host compose deploy without TLS) browsers silently discard Secure cookies, so the access token, refresh token, and CSRF cookie all vanished after login — producing 401 Unauthorized on every GET and 403 "CSRF token missing or invalid" on every mutation. Add COOKIE_SECURE to ApiEnv: optional boolean, falls back to NODE_ENV === 'production' when unset. Controllers and middleware now read env.COOKIE_SECURE instead of the NODE_ENV shortcut. The compose file sets it to false by default with a comment to flip once TLS is in front; HTTPS deployments can override via .env or drop the override to pick up the secure default.
23 lines
976 B
TypeScript
23 lines
976 B
TypeScript
import { z } from 'zod';
|
|
|
|
const defaultSecret = /change[-_ ]?me/i;
|
|
|
|
export const ApiEnv = z.object({
|
|
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
|
PORT: z.coerce.number().int().positive().default(3001),
|
|
DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'),
|
|
JWT_SECRET: z
|
|
.string()
|
|
.min(32, 'JWT_SECRET must be at least 32 characters')
|
|
.refine((v) => !defaultSecret.test(v), {
|
|
message: 'JWT_SECRET still matches the default placeholder — generate a real secret',
|
|
}),
|
|
CLIENT_ORIGIN: z.string().url().default('http://localhost:5173'),
|
|
// Whether to mark auth + CSRF cookies Secure. Must be false for plain-HTTP
|
|
// deployments (browsers silently drop Secure cookies over http://). Leave
|
|
// unset to fall back to NODE_ENV === 'production'.
|
|
COOKIE_SECURE: z
|
|
.preprocess((v) => (typeof v === 'string' ? v === 'true' : v), z.boolean().optional()),
|
|
});
|
|
export type ApiEnv = z.infer<typeof ApiEnv>;
|