domains-client.tsx123 lines · main
1'use client';
2
3import { useCallback, useEffect, useState } from 'react';
4
5import type { AuthV2ProjectRow } from '../lib/auth-v2-types';
6
7interface DomainRow {
8 id: string;
9 origin: string;
10 isWildcard: boolean;
11}
12
13export function AuthDomainsClient({ projects }: { projects: AuthV2ProjectRow[] }) {
14 const enabled = projects.filter((p) => p.authEnabled);
15 const [projectId, setProjectId] = useState(enabled[0]?.id ?? '');
16 const [domains, setDomains] = useState<DomainRow[]>([]);
17 const [origin, setOrigin] = useState('https://');
18 const [err, setErr] = useState<string | null>(null);
19 const [pending, setPending] = useState(false);
20
21 const load = useCallback(async (id: string) => {
22 if (!id) return;
23 setErr(null);
24 const res = await fetch(`/api/v1/projects/${id}/auth/allowed-domains`, {
25 credentials: 'include',
26 });
27 if (!res.ok) {
28 setErr(`load failed (${res.status})`);
29 return;
30 }
31 const body = (await res.json()) as { domains: DomainRow[] };
32 setDomains(body.domains ?? []);
33 }, []);
34
35 useEffect(() => {
36 if (projectId) void load(projectId);
37 }, [projectId, load]);
38
39 async function add(): Promise<void> {
40 if (!projectId || !origin.trim()) return;
41 setPending(true);
42 setErr(null);
43 try {
44 const res = await fetch(`/api/v1/projects/${projectId}/auth/allowed-domains`, {
45 method: 'POST',
46 credentials: 'include',
47 headers: { 'content-type': 'application/json' },
48 body: JSON.stringify({ origin: origin.trim(), isWildcard: origin.includes('*.') }),
49 });
50 if (!res.ok) {
51 const body = (await res.json().catch(() => ({}))) as { message?: string };
52 throw new Error(body.message ?? `http ${res.status}`);
53 }
54 setOrigin('https://');
55 await load(projectId);
56 } catch (e) {
57 setErr(e instanceof Error ? e.message : 'add failed');
58 } finally {
59 setPending(false);
60 }
61 }
62
63 if (enabled.length === 0) {
64 return (
65 <p className="font-mono text-xs text-[var(--color-text-muted)]">
66 enable Auth on a project first.
67 </p>
68 );
69 }
70
71 return (
72 <div className="flex max-w-xl flex-col gap-4">
73 <label className="flex flex-col gap-1 font-mono text-xs">
74 <span className="text-[var(--color-text-muted)]">project</span>
75 <select
76 value={projectId}
77 onChange={(e) => setProjectId(e.target.value)}
78 className="rounded-md border bg-[var(--color-surface)] px-3 py-2"
79 style={{ borderColor: 'var(--auth-accent-border)' }}
80 >
81 {enabled.map((p) => (
82 <option key={p.id} value={p.id}>
83 {p.name}
84 </option>
85 ))}
86 </select>
87 </label>
88
89 <div className="flex flex-wrap gap-2">
90 <input
91 value={origin}
92 onChange={(e) => setOrigin(e.target.value)}
93 placeholder="https://your-app.com"
94 className="min-w-[16rem] flex-1 rounded-md border bg-[var(--color-surface)] px-3 py-2 font-mono text-xs"
95 style={{ borderColor: 'var(--auth-accent-border)' }}
96 />
97 <button
98 type="button"
99 disabled={pending}
100 onClick={() => void add()}
101 className="rounded-md px-3 py-2 font-mono text-xs font-medium text-black disabled:opacity-50"
102 style={{ background: '#FFFD74' }}
103 >
104 {pending ? 'adding…' : 'add domain'}
105 </button>
106 </div>
107
108 <ul className="flex flex-col gap-2">
109 {domains.map((d) => (
110 <li
111 key={d.id}
112 className="rounded-md border px-3 py-2 font-mono text-xs"
113 style={{ borderColor: 'var(--auth-accent-border)' }}
114 >
115 {d.origin}
116 {d.isWildcard ? ' · wildcard' : ''}
117 </li>
118 ))}
119 </ul>
120 {err ? <p className="font-mono text-xs text-[var(--color-error)]">{err}</p> : null}
121 </div>
122 );
123}