diff --git a/backend/src/auth.ts b/backend/src/auth.ts index 5060018..31bb994 100644 --- a/backend/src/auth.ts +++ b/backend/src/auth.ts @@ -4,7 +4,7 @@ import { organization } from "better-auth/plugins"; import { db } from "./db/index.js"; import * as authSchema from "./db/schema/auth.js"; -import { env, isProd } from "./env.js"; +import { env } from "./env.js"; import { ac, roles } from "./lib/access.js"; import { sendEmail } from "./lib/email.js"; @@ -85,7 +85,10 @@ export const auth = betterAuth({ }, advanced: { - useSecureCookies: isProd, + // Secure cookies only when actually served over HTTPS — otherwise the + // browser silently drops them over http://localhost and login "does + // nothing". This is correct regardless of NODE_ENV. + useSecureCookies: env.BETTER_AUTH_URL.startsWith("https://"), defaultCookieAttributes: { sameSite: "lax" }, ipAddress: { ipAddressHeaders: ["x-forwarded-for", "x-real-ip"] }, }, diff --git a/backend/src/env.ts b/backend/src/env.ts index a6b0a3b..d616951 100644 --- a/backend/src/env.ts +++ b/backend/src/env.ts @@ -23,7 +23,14 @@ const schema = z.object({ SMTP_FROM: z.string().default("temetro "), }); -const parsed = schema.safeParse(process.env); +// 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