fix(web): make session cookie secure flag conditional on production

Setting Secure on http://localhost cookies works in Chrome (localhost
exception) but Firefox/Safari silently drop them, so dev users hit
'redirect to /login on every click' after a 'successful' login. Switch
to secure: NODE_ENV === 'production'. Public deploy still gets
Secure-only.

Also swap the login footer copy from a CLI hint to 'Forget Password?
Contact IT' — operator-friendly, doesn't leak the bootstrap
mechanism on the public sign-in screen.

Test updated to assert secure=true under prod NODE_ENV and a new test
locks in secure=false in dev.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-05-10 18:19:59 +08:00
parent 7ab51335a4
commit ebbbdbdfb8
3 changed files with 45 additions and 18 deletions

View File

@ -79,6 +79,10 @@ function fd(fields: Record<string, string>): FormData {
describe("loginAction", () => {
it("issues a session cookie when credentials are correct", async () => {
findUserMock.mockResolvedValue(ADMIN_ROW);
const prevEnv = process.env.NODE_ENV;
// @ts-expect-error - test override
process.env.NODE_ENV = "production";
try {
const r = await loginAction(fd({ username: "admin", password: "correct-horse" })).catch(
(e) => e,
);
@ -94,6 +98,25 @@ describe("loginAction", () => {
path: "/",
maxAge: 30 * 86400,
});
} finally {
// @ts-expect-error - test restore
process.env.NODE_ENV = prevEnv;
}
});
it("sets secure=false on the cookie when NODE_ENV !== production", async () => {
findUserMock.mockResolvedValue(ADMIN_ROW);
const prevEnv = process.env.NODE_ENV;
// @ts-expect-error - test override
process.env.NODE_ENV = "development";
try {
await loginAction(fd({ username: "admin", password: "correct-horse" })).catch(() => {});
const [, , attrs] = cookiesSetMock.mock.calls[0]!;
expect(attrs).toMatchObject({ secure: false });
} finally {
// @ts-expect-error - test restore
process.env.NODE_ENV = prevEnv;
}
});
it("returns ok:false on wrong password and does NOT set a cookie", async () => {

View File

@ -98,9 +98,14 @@ export async function loginAction(formData: FormData): Promise<LoginResult> {
secret,
);
const jar = await cookies();
// Secure: only require https in production. In dev we hit
// http://localhost:9000 directly, and Firefox/Safari silently drop
// Set-Cookie when Secure is set on http origins (Chrome has a
// localhost exception, others don't), which manifested as the
// session cookie never being persisted across requests.
jar.set(COOKIE_NAME, cookie, {
httpOnly: true,
secure: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: DEFAULT_TTL_SECONDS,

View File

@ -59,8 +59,7 @@ export function LoginFormClient({ next }: { next: string }) {
Sign in
</Button>
<p className="text-xs text-muted-foreground text-center">
First time? Run <code>./scripts/set-password.sh &lt;username&gt;</code>{" "}
in your tools container.
Forget Password? Contact IT
</p>
</form>
);