File size: 2,744 Bytes
57a889c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { Request, Response } from 'express';
import { SESSION_DURATION_MS, SESSION_DURATION_REMEMBER_MS } from '../config';

const COOKIE_NAME = 'trek_session';

/**
 * Controls the cookie lifetime for a login:
 *  - `undefined` β†’ persistent `maxAge: SESSION_DURATION_MS` (the historical
 *    default, used by register/demo and anything that doesn't opt in).
 *  - `true`  β†’ persistent `maxAge: SESSION_DURATION_REMEMBER_MS` ("Remember me").
 *  - `false` β†’ no `maxAge` β€” a browser-session cookie cleared on browser close.
 */
export type RememberOption = boolean | undefined;

/**
 * Decide whether the session cookie should carry the `Secure` flag.
 *
 * We previously only derived this from `NODE_ENV=production` or
 * `FORCE_HTTPS=true`. That left behind a common self-host setup:
 * TREK running behind Traefik / Caddy / Cloudflare Tunnel with
 * `NODE_ENV=development` locally and no `FORCE_HTTPS` β€” the cookie
 * went out without `Secure`, even though the public leg was https.
 *
 * Now we also honour `req.secure`, which Express derives from
 * `X-Forwarded-Proto` once `trust proxy` is set (TREK sets it to `1`
 * in production automatically). If Express sees the request was TLS
 * on the outermost hop, the cookie is `Secure`. `COOKIE_SECURE=false`
 * remains the explicit escape hatch for plain-HTTP LAN testing.
 */
export function cookieOptions(clear = false, req?: Request, remember?: RememberOption) {
  if (process.env.COOKIE_SECURE?.toLowerCase() === 'false') {
    return buildOptions(clear, false, remember);
  }
  const envSecure = process.env.NODE_ENV?.toLowerCase() === 'production' || process.env.FORCE_HTTPS?.toLowerCase() === 'true';
  const requestSecure = req?.secure === true;
  return buildOptions(clear, envSecure || requestSecure, remember);
}

function resolveMaxAge(remember: RememberOption): { maxAge: number } | Record<string, never> {
  // false β†’ session cookie (omit maxAge); true β†’ the longer "remember me"
  // window; undefined β†’ the historical default. Each maxAge matches the JWT exp.
  if (remember === false) return {};
  if (remember === true) return { maxAge: SESSION_DURATION_REMEMBER_MS };
  return { maxAge: SESSION_DURATION_MS };
}

function buildOptions(clear: boolean, secure: boolean, remember?: RememberOption) {
  return {
    httpOnly: true,
    secure,
    sameSite: 'lax' as const,
    path: '/',
    ...(clear ? {} : resolveMaxAge(remember)),
  };
}

export function setAuthCookie(res: Response, token: string, req?: Request, remember?: RememberOption): void {
  res.cookie(COOKIE_NAME, token, cookieOptions(false, req, remember));
}

export function clearAuthCookie(res: Response, req?: Request): void {
  res.clearCookie(COOKIE_NAME, cookieOptions(true, req));
}