| 1 | import { redirect } from "react-router"; |
| 2 | |
| 3 | import { isNamespaceShaped } from "@g1t/contracts"; |
| 4 | |
| 5 | import type { Viewer } from "@g1t/contracts"; |
| 6 | |
| 7 | import { identity, repos } from "./services.server"; |
| 8 | import { underWorkspace } from "./workspace-nav"; |
| 9 | |
| 10 | /** |
| 11 | * A workspace that was renamed keeps its old address as a redirect for |
| 12 | * `SLUG_HOLD_DAYS`, and an alias g1t's staff set (identity's aliases.rs: |
| 13 | * `g1t` for `flagon-io`) leads to its workspace for good. Call this where a |
| 14 | * page is about to 404 on its first path segment: if `slug` is an old name |
| 15 | * or an alias, it throws a 301 to the same page (path and query) under the |
| 16 | * workspace's slug now. Otherwise it returns, and the caller 404s as |
| 17 | * before. Only the not-found path pays for the lookup. |
| 18 | */ |
| 19 | export async function redirectIfRenamed(request: Request, slug: string): Promise<void> { |
| 20 | const old = slug.toLowerCase(); |
| 21 | // Nothing that could never have been a workspace's name or an alias is |
| 22 | // looked up. Reserved names such as `g1t` can be aliases. |
| 23 | if (!isNamespaceShaped(old)) return; |
| 24 | let current: string | null = null; |
| 25 | try { |
| 26 | current = await identity.resolveSlug(old); |
| 27 | } catch { |
| 28 | // A lookup that fails leaves the page a 404, never a 500. |
| 29 | return; |
| 30 | } |
| 31 | if (!current || current === old) return; |
| 32 | const url = new URL(request.url); |
| 33 | throw redirect(underWorkspace(url.pathname, url.search, current), 301); |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * A repository transferred to another workspace keeps its old address as a |
| 38 | * redirect until a repository is made there. Call this where a project page |
| 39 | * is about to 404: if `owner/repo` is a path a repository left, and the |
| 40 | * viewer may see it where it is now, it throws a 301 to the same page |
| 41 | * there. A private repository's new address is never shown to someone who |
| 42 | * cannot see it; they get the 404 as before. |
| 43 | */ |
| 44 | export async function redirectIfTransferred( |
| 45 | request: Request, |
| 46 | owner: string, |
| 47 | repo: string, |
| 48 | viewer: Viewer, |
| 49 | ): Promise<void> { |
| 50 | let now: { namespace: string; name: string } | null = null; |
| 51 | try { |
| 52 | now = await repos.resolvePath({ namespace: owner, name: repo }); |
| 53 | if (!now || !(await repos.get(now, viewer)).ok) return; |
| 54 | } catch { |
| 55 | return; |
| 56 | } |
| 57 | const url = new URL(request.url); |
| 58 | const segments = url.pathname.split("/"); |
| 59 | segments[1] = now.namespace; |
| 60 | segments[2] = now.name; |
| 61 | throw redirect(segments.join("/") + url.search, 301); |
| 62 | } |