page.tsx127 lines · main
1import { redirect } from 'next/navigation';
2
3import { apiJson } from '../../../lib/api';
4import { allow, deny } from './actions';
5
6/**
7 * `/v1/me` returns a flat profile `{ id, email, ... }` (see apps/api meRouter).
8 * Tolerate a nested `{ user }` shape in case older proxies wrap it.
9 */
10function profileFromMe(data: unknown): { id: string; email: string } | null {
11 if (!data || typeof data !== 'object') return null;
12 const rec = data as Record<string, unknown>;
13 if (typeof rec.id === 'string' && typeof rec.email === 'string') {
14 return { id: rec.id, email: rec.email };
15 }
16 if (rec.user && typeof rec.user === 'object') {
17 const u = rec.user as Record<string, unknown>;
18 if (typeof u.id === 'string' && typeof u.email === 'string') {
19 return { id: u.id, email: u.email };
20 }
21 }
22 return null;
23}
24
25function isLoopbackHttp(url: string): boolean {
26 try {
27 const u = new URL(url);
28 return (
29 u.protocol === 'http:'
30 && (u.hostname === '127.0.0.1' || u.hostname === 'localhost')
31 && u.port.length > 0
32 );
33 } catch {
34 return false;
35 }
36}
37
38function mintErrorMessage(code: string): string {
39 if (code === 'no_token' || code.startsWith('mint_failed')) {
40 return 'could not create a CLI token — try again, or sign out and sign in once more';
41 }
42 if (code === 'bad_request') {
43 return 'this link is incomplete — run the CLI again so it opens a fresh Allow page';
44 }
45 return `authorization failed (${code}) — run the CLI again`;
46}
47
48export default async function CliAuthPage({
49 searchParams,
50}: {
51 searchParams: Promise<{
52 redirect?: string;
53 state?: string;
54 host?: string;
55 error?: string;
56 }>;
57}) {
58 const { redirect: redirectUrl, state, host, error } = await searchParams;
59
60 if (!redirectUrl || !state) {
61 return <ErrorCard reason="missing redirect or state query param" />;
62 }
63 if (!isLoopbackHttp(redirectUrl)) {
64 return <ErrorCard reason="redirect must be a local-loopback http URL" />;
65 }
66 if (state.length > 256) {
67 return <ErrorCard reason="state too long" />;
68 }
69
70 let user: { id: string; email: string } | null = null;
71 try {
72 const data = await apiJson<unknown>('/v1/me');
73 user = profileFromMe(data);
74 } catch {
75 user = null;
76 }
77
78 if (!user) {
79 const back = `/cli-auth?redirect=${encodeURIComponent(redirectUrl)}&state=${encodeURIComponent(state)}${host ? `&host=${encodeURIComponent(host)}` : ''}`;
80 redirect(`/signin?next=${encodeURIComponent(back)}`);
81 }
82
83 return (
84 <div className="flex flex-col gap-4 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-6 font-mono text-sm">
85 <h1 className="text-base">authorize the briven cli?</h1>
86 <p className="text-[var(--color-text-muted)]">
87 signed in as <strong>{user.email}</strong>
88 {host ? ` · machine ${host}` : null}
89 </p>
90 <p className="text-xs text-[var(--color-text-muted)]">
91 issues a 24-hour session token to the cli on your laptop. revoke with{' '}
92 <code>briven logout</code>.
93 </p>
94 {error ? (
95 <p
96 className="rounded border border-[var(--color-error)] px-3 py-2 text-xs text-[var(--color-error)]"
97 role="alert"
98 >
99 {mintErrorMessage(error)}
100 </p>
101 ) : null}
102 <form className="flex gap-2">
103 <button
104 formAction={allow.bind(null, { redirectUrl, state })}
105 className="flex-1 rounded-md bg-[var(--color-primary)] px-3 py-2 text-[var(--color-text-inverse)]"
106 >
107 allow
108 </button>
109 <button
110 formAction={deny.bind(null, { redirectUrl, state })}
111 className="flex-1 rounded-md border border-[var(--color-border)] px-3 py-2 text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
112 >
113 deny
114 </button>
115 </form>
116 </div>
117 );
118}
119
120function ErrorCard({ reason }: { reason: string }) {
121 return (
122 <div className="rounded-md border border-[var(--color-error)] bg-[var(--color-surface)] p-6 font-mono text-sm text-[var(--color-error)]">
123 <h1 className="text-base">this URL was not opened by the briven cli</h1>
124 <p className="mt-2 text-xs">close this tab. ({reason})</p>
125 </div>
126 );
127}