| 1 | import { headers } from "next/headers"; |
| 2 | import { notFound, redirect |
| 3 | import { cache } |
| 4 | import createFetchClient from "openapi-fetch |
| 5 | |
| 6 | import { authClient } |
| 7 | import { administers } |
| 8 | import { unwrap } |
| 9 | import { INTERNAL_API_URL } |
| 10 | import { splitRevision } |
| 11 | |
| 12 | import type { paths |
| 13 | |
| 14 | /** What the API needs from the browser's request: its cookies, and the client address the proxy set (x-real-ip on Railway, x-forwarded-for behind Caddy), which Better Auth and code search rate-limit by. */ |
| 15 | async function forwardedHeaders() |
| 16 | const incoming = |
| 17 | const forwarded: |
| 18 | for (const name |
| 19 | const value = |
| 20 | if (value) forwarded |
| 21 | } |
| 22 | return forwarded; |
| 23 | } |
| 24 | |
| 25 | /** 404s when the API says the resource is absent, or that it is not public and nobody is signed in: a page must not tell an anonymous visitor which private repositories exist. */ |
| 26 | export function notFoundIfHidden(response |
| 27 | if (response.status |
| 28 | } |
| 29 | |
| 30 | /** Typed API client for server components. `credentials: "include"` is a browser concept, so the incoming request's cookies are forwarded explicitly. */ |
| 31 | export async function createServerClient |
| 32 | return createFetchClient<paths |
| 33 | baseUrl: INTERNAL_API_URL, |
| 34 | headers: await forwardedHeaders |
| 35 | }); |
| 36 | } |
| 37 | |
| 38 | type ServerClient |
| 39 | |
| 40 | /** Calls the API as the viewer from a server action and returns its data, throwing the API's error message for next-safe-action to report. */ |
| 41 | export async function callApi |
| 42 | request: (client |
| 43 | ): Promise<T |
| 44 | return unwrap(request |
| 45 | } |
| 46 | |
| 47 | /** The signed-in session, read over INTERNAL_API_URL: in Docker the public API origin can be this container's own localhost. Fetched once per render, however many layouts and pages ask. */ |
| 48 | export const getServerSession = |
| 49 | async (): Promise |
| 50 | const forwarded = |
| 51 | if (!forwarded.cookie |
| 52 | |
| 53 | const res = |
| 54 | headers: forwarded, |
| 55 | cache: "no-store", |
| 56 | }); |
| 57 | return res.ok |
| 58 | }, |
| 59 | ); |
| 60 | |
| 61 | /** The signed-in viewer's username, sending anyone signed out to sign in and back to `path`. */ |
| 62 | export async function requireViewer |
| 63 | const username = |
| 64 | if (!username) |
| 65 | redirect(`/auth/sign-in?redirectTo=${encodeURIComponent |
| 66 | return username; |
| 67 | } |
| 68 | |
| 69 | /** The viewer's role on a repository, fetched once per render however many components ask. */ |
| 70 | export const getViewerRole = |
| 71 | const client = |
| 72 | const { data |
| 73 | params: { path: { |
| 74 | }); |
| 75 | return data?.viewerRole |
| 76 | }); |
| 77 | |
| 78 | /** The organizations the viewer belongs to, fetched once per render however many components ask. */ |
| 79 | const getViewerOrganizations = cache |
| 80 | const client = |
| 81 | const { data |
| 82 | return data?.organizations |
| 83 | }); |
| 84 | |
| 85 | /** Slugs of the organizations the viewer administers, where they may create, fork and transfer repositories to. */ |
| 86 | export const getAdminOrganizations = |
| 87 | (await getViewerOrganizations()) |
| 88 | .filter((organization) |
| 89 | .map((organization) |
| 90 | ); |
| 91 | |
| 92 | /** The viewer's role in an organization, or null outside it. */ |
| 93 | export async function getOrganizationRole |
| 94 | return ( |
| 95 | (await getViewerOrganizations()). |
| 96 | (organization) => |
| 97 | )?.viewerRole ?? null |
| 98 | ); |
| 99 | } |
| 100 | |
| 101 | /** Branch names of a repository, fetched once per render however many components ask. */ |
| 102 | export const getBranchNames = |
| 103 | const client = |
| 104 | const { data |
| 105 | "/api/repositories/{username}/{slug}/branches", |
| 106 | { params: { path: |
| 107 | ); |
| 108 | return data?.branches |
| 109 | }); |
| 110 | |
| 111 | /** The revision and path a `[ref]/[[...path]]` page names, where a branch like `feat/x` spans more than one segment. Pages get params still encoded; route handlers get them decoded and call `splitRevision` themselves. */ |
| 112 | export async function resolveRevisionPath |
| 113 | username: string, |
| 114 | slug: string, |
| 115 | ref: string, |
| 116 | path: string[] |
| 117 | ) { |
| 118 | return splitRevision( |
| 119 | [ref, ...path |
| 120 | await getBranchNames(username |
| 121 | ); |
| 122 | } |