Skip to content

g1t/apps/web/app/lib/renamed.server.ts

62 lines2,374 bytesCodeBlame
1import { redirect } from "react-router";
2
3import { isNamespaceShaped } from "@g1t/contracts";
4
5import type { Viewer } from "@g1t/contracts";
6
7import { identity, repos } from "./services.server";
8import { 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 */
19export 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 */
44export 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}