providers-form.tsx323 lines · main
1'use client';
2
3/**
4 * briven-engine — save Google/GitHub (etc.) secrets for a project.
5 * Uses session cookies; shows configured state after save.
6 */
7
8import { useCallback, useEffect, useState } from 'react';
9
10const PROVIDERS = [
11 { id: 'google', name: 'Google' },
12 { id: 'github', name: 'GitHub' },
13 { id: 'discord', name: 'Discord' },
14 { id: 'microsoft', name: 'Microsoft' },
15 { id: 'apple', name: 'Apple' },
16] as const;
17
18type ProviderStatus = {
19 thirdPartyId: string;
20 name: string;
21 configured: boolean;
22 hasClientId: boolean;
23 hasClientSecret: boolean;
24};
25
26function apiOrigin(): string {
27 return (
28 process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN?.replace(/\/$/, '') ||
29 'https://api.briven.tech'
30 );
31}
32
33export function BrivenEngineProvidersForm() {
34 const [projectId, setProjectId] = useState('');
35 const [thirdPartyId, setThirdPartyId] = useState<string>('google');
36 const [clientId, setClientId] = useState('');
37 const [clientSecret, setClientSecret] = useState('');
38 const [status, setStatus] = useState<string | null>(null);
39 const [busy, setBusy] = useState(false);
40 const [providers, setProviders] = useState<ProviderStatus[]>([]);
41
42 const loadConfig = useCallback(async (pid: string) => {
43 if (!pid.trim()) {
44 setProviders([]);
45 return;
46 }
47 try {
48 const res = await fetch(
49 `${apiOrigin()}/v1/auth-core/projects/${encodeURIComponent(pid.trim())}/config`,
50 { credentials: 'include', cache: 'no-store' },
51 );
52 if (!res.ok) {
53 setProviders([]);
54 return;
55 }
56 const body = (await res.json()) as { providers?: ProviderStatus[] };
57 setProviders(body.providers ?? []);
58 } catch {
59 setProviders([]);
60 }
61 }, []);
62
63 useEffect(() => {
64 const t = setTimeout(() => {
65 void loadConfig(projectId);
66 }, 400);
67 return () => clearTimeout(t);
68 }, [projectId, loadConfig]);
69
70 async function onSubmit(e: React.FormEvent) {
71 e.preventDefault();
72 setStatus(null);
73 if (!projectId.trim() || !clientId.trim() || !clientSecret.trim()) {
74 setStatus('Fill project id, client id, and client secret.');
75 return;
76 }
77 setBusy(true);
78 try {
79 const res = await fetch(
80 `${apiOrigin()}/v1/auth-core/projects/${encodeURIComponent(projectId.trim())}/providers/${encodeURIComponent(thirdPartyId)}`,
81 {
82 method: 'PUT',
83 headers: { 'content-type': 'application/json' },
84 credentials: 'include',
85 body: JSON.stringify({ clientId, clientSecret }),
86 },
87 );
88 const body = (await res.json().catch(() => ({}))) as {
89 ok?: boolean;
90 message?: string;
91 engine?: string;
92 config?: { providers?: ProviderStatus[] };
93 };
94 if (!res.ok) {
95 setStatus(
96 body.message ??
97 `Save failed (${res.status}). Sign in as project admin.`,
98 );
99 } else {
100 setStatus(`Saved ${thirdPartyId} secrets for this project.`);
101 setClientSecret('');
102 if (body.config?.providers) setProviders(body.config.providers);
103 else await loadConfig(projectId);
104 }
105 } catch (err) {
106 setStatus(err instanceof Error ? err.message : 'Network error');
107 } finally {
108 setBusy(false);
109 }
110 }
111
112 return (
113 <div className="flex flex-col gap-4">
114 <form
115 onSubmit={onSubmit}
116 className="flex max-w-lg flex-col gap-3 rounded-md border p-4"
117 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
118 >
119 <p className="font-mono text-sm text-[var(--color-text)]">
120 social sign-in secrets
121 </p>
122 <p className="font-mono text-[10px] text-[var(--color-text-muted)]">
123 Google / GitHub client id + secret for this project. Stored encrypted.
124 Used when users click “Sign in with…”.
125 </p>
126 <label className="flex flex-col gap-1 font-mono text-xs text-[var(--color-text-muted)]">
127 Project id
128 <input
129 className="rounded border bg-transparent px-2 py-1.5 text-[var(--color-text)]"
130 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
131 value={projectId}
132 onChange={(e) => setProjectId(e.target.value)}
133 placeholder="p_…"
134 autoComplete="off"
135 />
136 </label>
137 <label className="flex flex-col gap-1 font-mono text-xs text-[var(--color-text-muted)]">
138 Provider
139 <select
140 className="rounded border bg-transparent px-2 py-1.5 text-[var(--color-text)]"
141 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
142 value={thirdPartyId}
143 onChange={(e) => setThirdPartyId(e.target.value)}
144 >
145 {PROVIDERS.map((p) => (
146 <option key={p.id} value={p.id}>
147 {p.name}
148 </option>
149 ))}
150 </select>
151 </label>
152 <label className="flex flex-col gap-1 font-mono text-xs text-[var(--color-text-muted)]">
153 Client id
154 <input
155 className="rounded border bg-transparent px-2 py-1.5 text-[var(--color-text)]"
156 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
157 value={clientId}
158 onChange={(e) => setClientId(e.target.value)}
159 autoComplete="off"
160 placeholder={
161 thirdPartyId === 'google'
162 ? '….apps.googleusercontent.com'
163 : 'OAuth app client id'
164 }
165 />
166 </label>
167 <label className="flex flex-col gap-1 font-mono text-xs text-[var(--color-text-muted)]">
168 Client secret
169 <input
170 type="password"
171 className="rounded border bg-transparent px-2 py-1.5 text-[var(--color-text)]"
172 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
173 value={clientSecret}
174 onChange={(e) => setClientSecret(e.target.value)}
175 autoComplete="off"
176 />
177 </label>
178 <button
179 type="submit"
180 disabled={busy}
181 className="rounded px-3 py-2 font-mono text-xs font-medium text-black disabled:opacity-50"
182 style={{ background: 'var(--auth-accent, #FFFD74)' }}
183 >
184 {busy ? 'Saving…' : 'Save secrets'}
185 </button>
186 {status ? (
187 <p className="font-mono text-[10px] text-[var(--color-text-muted)]">
188 {status}
189 </p>
190 ) : null}
191 </form>
192
193 {providers.length > 0 ? (
194 <div
195 className="max-w-lg rounded-md border p-3 font-mono text-xs"
196 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
197 >
198 <p className="text-[10px] uppercase text-[var(--color-text-muted)]">
199 configured for this project
200 </p>
201 <ul className="mt-2 flex flex-col gap-1">
202 {providers.map((p) => (
203 <li key={p.thirdPartyId} className="text-[var(--color-text)]">
204 {p.name} ·{' '}
205 <span className="text-[var(--color-text-muted)]">
206 {p.configured ? 'ready' : 'not set'}
207 </span>
208 </li>
209 ))}
210 </ul>
211 </div>
212 ) : null}
213 </div>
214 );
215}
216
217export function BrivenEngineSmsForm() {
218 const [projectId, setProjectId] = useState('');
219 const [accountSid, setAccountSid] = useState('');
220 const [authToken, setAuthToken] = useState('');
221 const [fromNumber, setFromNumber] = useState('');
222 const [status, setStatus] = useState<string | null>(null);
223 const [busy, setBusy] = useState(false);
224
225 async function onSubmit(e: React.FormEvent) {
226 e.preventDefault();
227 setStatus(null);
228 if (!projectId.trim() || !accountSid || !authToken || !fromNumber) {
229 setStatus('Fill all SMS fields.');
230 return;
231 }
232 setBusy(true);
233 try {
234 const res = await fetch(
235 `${apiOrigin()}/v1/auth-core/projects/${encodeURIComponent(projectId.trim())}/delivery/sms`,
236 {
237 method: 'PUT',
238 headers: { 'content-type': 'application/json' },
239 credentials: 'include',
240 body: JSON.stringify({ accountSid, authToken, fromNumber }),
241 },
242 );
243 const body = (await res.json().catch(() => ({}))) as {
244 ok?: boolean;
245 message?: string;
246 engine?: string;
247 };
248 if (!res.ok) {
249 setStatus(body.message ?? `Save failed (${res.status})`);
250 } else {
251 setStatus('SMS secrets saved.');
252 setAuthToken('');
253 }
254 } catch (err) {
255 setStatus(err instanceof Error ? err.message : 'Network error');
256 } finally {
257 setBusy(false);
258 }
259 }
260
261 return (
262 <form
263 onSubmit={onSubmit}
264 className="flex max-w-lg flex-col gap-3 rounded-md border p-4"
265 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
266 >
267 <p className="font-mono text-sm text-[var(--color-text)]">SMS codes</p>
268 <p className="font-mono text-[10px] text-[var(--color-text-muted)]">
269 Twilio-compatible: account SID, auth token, from number (E.164).
270 </p>
271 <label className="flex flex-col gap-1 font-mono text-xs text-[var(--color-text-muted)]">
272 Project id
273 <input
274 className="rounded border bg-transparent px-2 py-1.5 text-[var(--color-text)]"
275 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
276 value={projectId}
277 onChange={(e) => setProjectId(e.target.value)}
278 placeholder="p_…"
279 />
280 </label>
281 <label className="flex flex-col gap-1 font-mono text-xs text-[var(--color-text-muted)]">
282 Account SID
283 <input
284 className="rounded border bg-transparent px-2 py-1.5 text-[var(--color-text)]"
285 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
286 value={accountSid}
287 onChange={(e) => setAccountSid(e.target.value)}
288 />
289 </label>
290 <label className="flex flex-col gap-1 font-mono text-xs text-[var(--color-text-muted)]">
291 Auth token
292 <input
293 type="password"
294 className="rounded border bg-transparent px-2 py-1.5 text-[var(--color-text)]"
295 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
296 value={authToken}
297 onChange={(e) => setAuthToken(e.target.value)}
298 />
299 </label>
300 <label className="flex flex-col gap-1 font-mono text-xs text-[var(--color-text-muted)]">
301 From number
302 <input
303 className="rounded border bg-transparent px-2 py-1.5 text-[var(--color-text)]"
304 style={{ borderColor: 'var(--auth-accent-border, var(--color-border))' }}
305 value={fromNumber}
306 onChange={(e) => setFromNumber(e.target.value)}
307 placeholder="+1…"
308 />
309 </label>
310 <button
311 type="submit"
312 disabled={busy}
313 className="rounded px-3 py-2 font-mono text-xs font-medium text-black disabled:opacity-50"
314 style={{ background: 'var(--auth-accent, #FFFD74)' }}
315 >
316 {busy ? 'Saving…' : 'Save SMS'}
317 </button>
318 {status ? (
319 <p className="font-mono text-[10px] text-[var(--color-text-muted)]">{status}</p>
320 ) : null}
321 </form>
322 );
323}