| 1 | /** |
| 2 | * Where to send someone after they sign in, sign up or switch account: |
| 3 | * a path on g1t, and never anywhere else. |
| 4 | * |
| 5 | * Only a plain same-origin path is honoured. `//host`, `/\host` and paths |
| 6 | * with control characters are refused, because browsers read all of them |
| 7 | * as another site (they drop tabs and newlines, and treat `\` as `/`). |
| 8 | */ |
| 9 | export function safeNext(raw: string | null | undefined): string { |
| 10 | if (!raw || !raw.startsWith("/")) return "/"; |
| 11 | if (raw.startsWith("//") || raw.includes("\\")) return "/"; |
| 12 | // Control characters and DEL, which a browser may strip before parsing. |
| 13 | if (/[\u0000-\u001f\u007f]/.test(raw)) return "/"; |
| 14 | try { |
| 15 | const base = "https://g1t.invalid"; |
| 16 | const url = new URL(raw, base); |
| 17 | if (url.origin !== base) return "/"; |
| 18 | return url.pathname + url.search + url.hash; |
| 19 | } catch { |
| 20 | return "/"; |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | /** A sign-in or sign-up page that brings someone back to `here` afterwards. */ |
| 25 | export function withNext(page: "/login" | "/register", here: string): string { |
| 26 | const next = safeNext(here); |
| 27 | return next === "/" ? page : `${page}?next=${encodeURIComponent(next)}`; |
| 28 | } |