roles-form.tsx219 lines · main
1'use client';
2
3import { useRouter } from 'next/navigation';
4import { useState, type FormEvent } from 'react';
5
6/**
7 * Create a role from the yellow Auth security tab.
8 * Calls /api/v1/auth-core/roles (proxied to the API with your cookie).
9 */
10export function AuthRolesForm({ projectId }: { projectId?: string }) {
11 const router = useRouter();
12 const [role, setRole] = useState('');
13 const [permissions, setPermissions] = useState('');
14 const [pending, setPending] = useState(false);
15 const [err, setErr] = useState<string | null>(null);
16 const [okMsg, setOkMsg] = useState<string | null>(null);
17
18 async function onSubmit(e: FormEvent) {
19 e.preventDefault();
20 const name = role.trim();
21 if (!name) return;
22 setPending(true);
23 setErr(null);
24 setOkMsg(null);
25 try {
26 const perms = permissions
27 .split(',')
28 .map((p) => p.trim())
29 .filter(Boolean);
30 const res = await fetch('/api/v1/auth-core/roles', {
31 method: 'POST',
32 credentials: 'include',
33 headers: { 'content-type': 'application/json' },
34 body: JSON.stringify({
35 role: name,
36 permissions: perms,
37 ...(projectId ? { projectId } : {}),
38 }),
39 });
40 const body = (await res.json().catch(() => ({}))) as {
41 ok?: boolean;
42 message?: string;
43 code?: string;
44 };
45 if (!res.ok || body.ok === false) {
46 throw new Error(
47 body.message ?? body.code ?? `could not create role (${res.status})`,
48 );
49 }
50 setRole('');
51 setPermissions('');
52 setOkMsg(body.message === 'updated' ? 'role updated' : 'role created');
53 router.refresh();
54 } catch (e) {
55 setErr(e instanceof Error ? e.message : 'create failed');
56 } finally {
57 setPending(false);
58 }
59 }
60
61 const [userId, setUserId] = useState('');
62 const [assignBusy, setAssignBusy] = useState(false);
63
64 async function assignOrUnassign(mode: 'assign' | 'unassign') {
65 const name = role.trim();
66 const uid = userId.trim();
67 if (!name || !uid) {
68 setErr('role name and user id required to assign/unassign');
69 return;
70 }
71 setAssignBusy(true);
72 setErr(null);
73 setOkMsg(null);
74 try {
75 const res = await fetch(`/api/v1/auth-core/roles/${mode}`, {
76 method: 'POST',
77 credentials: 'include',
78 headers: { 'content-type': 'application/json' },
79 body: JSON.stringify({
80 role: name,
81 userId: uid,
82 ...(projectId ? { projectId } : {}),
83 }),
84 });
85 const body = (await res.json().catch(() => ({}))) as {
86 ok?: boolean;
87 message?: string;
88 code?: string;
89 };
90 if (!res.ok || body.ok === false) {
91 throw new Error(body.message ?? body.code ?? `${mode} failed (${res.status})`);
92 }
93 setOkMsg(body.message ?? mode);
94 router.refresh();
95 } catch (e) {
96 setErr(e instanceof Error ? e.message : `${mode} failed`);
97 } finally {
98 setAssignBusy(false);
99 }
100 }
101
102 async function deleteRole() {
103 const name = role.trim();
104 if (!name) return;
105 setPending(true);
106 setErr(null);
107 setOkMsg(null);
108 try {
109 const res = await fetch('/api/v1/auth-core/roles', {
110 method: 'DELETE',
111 credentials: 'include',
112 headers: { 'content-type': 'application/json' },
113 body: JSON.stringify({
114 role: name,
115 ...(projectId ? { projectId } : {}),
116 }),
117 });
118 const body = (await res.json().catch(() => ({}))) as {
119 ok?: boolean;
120 message?: string;
121 code?: string;
122 };
123 if (!res.ok || body.ok === false) {
124 throw new Error(body.message ?? body.code ?? `delete failed (${res.status})`);
125 }
126 setRole('');
127 setOkMsg(body.message ?? 'deleted');
128 router.refresh();
129 } catch (e) {
130 setErr(e instanceof Error ? e.message : 'delete failed');
131 } finally {
132 setPending(false);
133 }
134 }
135
136 return (
137 <form onSubmit={onSubmit} className="mt-4 space-y-3">
138 <div className="flex flex-col gap-2 sm:flex-row sm:items-end">
139 <label className="flex flex-1 flex-col gap-1">
140 <span className="font-mono text-[10px] uppercase tracking-widest text-[var(--color-text-muted)]">
141 role name
142 </span>
143 <input
144 value={role}
145 onChange={(e) => setRole(e.target.value)}
146 placeholder="admin"
147 className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg)] px-3 py-2 font-mono text-xs text-[var(--color-text)] outline-none focus:border-[color-mix(in_srgb,#FFFD74_50%,var(--color-border))]"
148 disabled={pending}
149 />
150 </label>
151 <label className="flex flex-[2] flex-col gap-1">
152 <span className="font-mono text-[10px] uppercase tracking-widest text-[var(--color-text-muted)]">
153 permissions (comma-separated)
154 </span>
155 <input
156 value={permissions}
157 onChange={(e) => setPermissions(e.target.value)}
158 placeholder="read, write, *"
159 className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg)] px-3 py-2 font-mono text-xs text-[var(--color-text)] outline-none focus:border-[color-mix(in_srgb,#FFFD74_50%,var(--color-border))]"
160 disabled={pending}
161 />
162 </label>
163 <button
164 type="submit"
165 disabled={pending || !role.trim()}
166 className="rounded-md px-4 py-2 font-mono text-xs font-medium disabled:opacity-50"
167 style={{ background: '#FFFD74', color: '#111' }}
168 >
169 {pending ? 'saving…' : 'save role'}
170 </button>
171 <button
172 type="button"
173 onClick={() => void deleteRole()}
174 disabled={pending || !role.trim()}
175 className="rounded-md border border-red-500/40 px-3 py-2 font-mono text-xs text-red-300 disabled:opacity-50"
176 >
177 delete role
178 </button>
179 </div>
180 <div className="flex flex-col gap-2 sm:flex-row sm:items-end">
181 <label className="flex flex-1 flex-col gap-1">
182 <span className="font-mono text-[10px] uppercase tracking-widest text-[var(--color-text-muted)]">
183 user id (beu_…)
184 </span>
185 <input
186 value={userId}
187 onChange={(e) => setUserId(e.target.value)}
188 placeholder="beu_…"
189 className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg)] px-3 py-2 font-mono text-xs text-[var(--color-text)] outline-none focus:border-[color-mix(in_srgb,#FFFD74_50%,var(--color-border))]"
190 disabled={assignBusy}
191 />
192 </label>
193 <button
194 type="button"
195 onClick={() => void assignOrUnassign('assign')}
196 disabled={assignBusy || !role.trim() || !userId.trim()}
197 className="rounded-md px-3 py-2 font-mono text-xs disabled:opacity-50"
198 style={{ background: '#FFFD74', color: '#111' }}
199 >
200 assign
201 </button>
202 <button
203 type="button"
204 onClick={() => void assignOrUnassign('unassign')}
205 disabled={assignBusy || !role.trim() || !userId.trim()}
206 className="rounded-md border border-[var(--color-border-subtle)] px-3 py-2 font-mono text-xs disabled:opacity-50"
207 >
208 unassign
209 </button>
210 </div>
211 {err ? (
212 <p className="font-mono text-xs text-red-400">{err}</p>
213 ) : null}
214 {okMsg ? (
215 <p className="font-mono text-xs text-[var(--color-text-muted)]">{okMsg}</p>
216 ) : null}
217 </form>
218 );
219}