import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { authorizeAction } from "@/app/actions/oauth";
import { LogoMark } from "@/components/brand";
import { AlertIcon } from "@/components/icons";
import { Alert, Blankslate, Box } from "@/components/ui";
import { requireViewer } from "@/lib/auth";
import { parseAuthorizeRequest } from "@/lib/oauth";
import { canWrite, listReposForUser, repoAccess } from "@/lib/repos";
import { AuthorizeForm } from "./form";
export const metadata: Metadata = { title: "Authorize application" };
/** Where the application sends the user afterwards, as they'd recognize it. */
function destination(redirectUri: string) {
const url = new URL(redirectUri);
if (url.protocol === "http:" || url.protocol === "https:") {
return ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) ? "an app on this computer" : url.host;
}
return `the ${url.protocol.slice(0, -1)}: app`;
}
/** An application (an MCP client, usually an AI agent) asks to act for the signed-in user. */
export default async function AuthorizePage({ searchParams }: PageProps<"/oauth/authorize">) {
// OAuth parameters appear once; a repeated one counts by its first value.
const params: Record<string, string> = {};
for (const [key, value] of Object.entries(await searchParams)) {
const first = Array.isArray(value) ? value[0] : value;
if (typeof first === "string") params[key] = first;
}
const viewer = await requireViewer(`/oauth/authorize?${new URLSearchParams(params)}`);
const parsed = await parseAuthorizeRequest(params);
if ("redirect" in parsed) redirect(parsed.redirect);
if ("fatal" in parsed) {
return (
<main className="mx-auto w-full max-w-[520px] px-4 py-16">
<Box>
<Blankslate icon={<AlertIcon />} title="This connection can’t continue">
{parsed.fatal}
</Blankslate>
</Box>
</main>
);
}
const { request } = parsed;
const repos = await Promise.all(
(await listReposForUser(viewer.id)).map(async (r) => ({
id: r.id,
fullName: `${r.owner}/${r.name}`,
writable: canWrite(await repoAccess(r, viewer.id)),
})),
);
return (
<main className="mx-auto w-full max-w-[560px] px-4 py-12">
<div className="text-center">
<span className="mx-auto flex size-16 items-center justify-center rounded-hero material-cap">
<LogoMark size={36} />
</span>
<h1 className="mt-6 text-[22px] leading-7 font-semibold tracking-[-0.025em] text-ink">Authorize {request.client.name}</h1>
<p className="mt-1 type-lead text-ink2">
It wants to work with your repositories as <b className="text-ink">@{viewer.username}</b>, through MCP.
</p>
</div>
<div className="mt-6">
<Alert kind="info">
The name above is chosen by the application itself. Only continue if you just connected an agent or app to Vectrum.
</Alert>
</div>
<Box className="mt-4">
<AuthorizeForm
action={authorizeAction.bind(null, params)}
requested={request.permission}
repos={repos}
destination={destination(request.redirectUri)}
/>
</Box>
</main>
);
}