advanced-auth-settings.tsx147 lines · main
1'use client';
2
3import { useCallback, useEffect, useState } from 'react';
4
5/**
6 * Product settings: custom JWT claims + username login (SuperTokens-class depth).
7 */
8export function AdvancedAuthSettings({ projectId }: { projectId: string }) {
9 const [claimsJson, setClaimsJson] = useState('{\n "tenant_plan": "pro"\n}');
10 const [usernameLogin, setUsernameLogin] = useState(false);
11 const [pending, setPending] = useState(false);
12 const [err, setErr] = useState<string | null>(null);
13 const [ok, setOk] = useState<string | null>(null);
14
15 const load = useCallback(async () => {
16 setErr(null);
17 const res = await fetch(
18 `/api/v1/auth-core/projects/${encodeURIComponent(projectId)}/config`,
19 { credentials: 'include', cache: 'no-store' },
20 );
21 if (!res.ok) return;
22 const body = (await res.json()) as {
23 jwtClaims?: Record<string, string | number | boolean>;
24 usernameLogin?: boolean;
25 };
26 if (body.jwtClaims && Object.keys(body.jwtClaims).length > 0) {
27 setClaimsJson(JSON.stringify(body.jwtClaims, null, 2));
28 }
29 setUsernameLogin(Boolean(body.usernameLogin));
30 }, [projectId]);
31
32 useEffect(() => {
33 void load();
34 }, [load]);
35
36 async function saveClaims(): Promise<void> {
37 setPending(true);
38 setErr(null);
39 setOk(null);
40 try {
41 const claims = JSON.parse(claimsJson) as Record<string, string | number | boolean>;
42 const res = await fetch(
43 `/api/v1/auth-core/projects/${encodeURIComponent(projectId)}/jwt-claims`,
44 {
45 method: 'PUT',
46 credentials: 'include',
47 headers: { 'content-type': 'application/json' },
48 body: JSON.stringify(claims),
49 },
50 );
51 if (!res.ok) {
52 const b = (await res.json().catch(() => ({}))) as { message?: string };
53 throw new Error(b.message ?? `save failed (${res.status})`);
54 }
55 setOk('JWT claim template saved — applied on new ID tokens');
56 await load();
57 } catch (e) {
58 setErr(e instanceof Error ? e.message : 'save failed');
59 } finally {
60 setPending(false);
61 }
62 }
63
64 async function saveUsername(enabled: boolean): Promise<void> {
65 setPending(true);
66 setErr(null);
67 setOk(null);
68 try {
69 const res = await fetch(
70 `/api/v1/auth-core/projects/${encodeURIComponent(projectId)}/username-login`,
71 {
72 method: 'PUT',
73 credentials: 'include',
74 headers: { 'content-type': 'application/json' },
75 body: JSON.stringify({ enabled }),
76 },
77 );
78 if (!res.ok) {
79 const b = (await res.json().catch(() => ({}))) as { message?: string };
80 throw new Error(b.message ?? `save failed (${res.status})`);
81 }
82 setUsernameLogin(enabled);
83 setOk(
84 enabled
85 ? 'username login on — users can sign in with metadata.username'
86 : 'username login off — email only',
87 );
88 } catch (e) {
89 setErr(e instanceof Error ? e.message : 'save failed');
90 } finally {
91 setPending(false);
92 }
93 }
94
95 return (
96 <div className="space-y-6">
97 <div>
98 <h3 className="font-mono text-sm text-[var(--color-text)]">
99 custom JWT claims
100 </h3>
101 <p className="mt-1 font-mono text-[11px] text-[var(--color-text-muted)]">
102 Extra fields merged into OIDC ID tokens for this project (string / number /
103 boolean only). Reserved claims like sub / iss are ignored.
104 </p>
105 <textarea
106 value={claimsJson}
107 onChange={(e) => setClaimsJson(e.target.value)}
108 rows={6}
109 spellCheck={false}
110 className="mt-3 w-full max-w-lg rounded-md border bg-[var(--color-surface)] px-3 py-2 font-mono text-[11px] text-[var(--color-text)]"
111 style={{ borderColor: 'var(--color-border)' }}
112 />
113 <button
114 type="button"
115 disabled={pending}
116 onClick={() => void saveClaims()}
117 className="mt-2 rounded-md px-3 py-1.5 font-mono text-xs font-medium text-black disabled:opacity-50"
118 style={{ background: '#FFFD74' }}
119 >
120 {pending ? 'saving…' : 'save JWT claims'}
121 </button>
122 </div>
123
124 <div>
125 <h3 className="font-mono text-sm text-[var(--color-text)]">username login</h3>
126 <p className="mt-1 font-mono text-[11px] text-[var(--color-text-muted)]">
127 When on, email + password sign-in also accepts a username stored on the user
128 (metadata.username).
129 </p>
130 <label className="mt-3 flex items-center gap-2 font-mono text-xs text-[var(--color-text)]">
131 <input
132 type="checkbox"
133 checked={usernameLogin}
134 disabled={pending}
135 onChange={(e) => void saveUsername(e.target.checked)}
136 />
137 allow username as login id
138 </label>
139 </div>
140
141 {ok ? (
142 <p className="font-mono text-xs text-[var(--color-text)]">{ok}</p>
143 ) : null}
144 {err ? <p className="font-mono text-xs text-red-400">{err}</p> : null}
145 </div>
146 );
147}