roles-form.tsx105 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 return (
62 <form onSubmit={onSubmit} className="mt-4 space-y-3">
63 <div className="flex flex-col gap-2 sm:flex-row sm:items-end">
64 <label className="flex flex-1 flex-col gap-1">
65 <span className="font-mono text-[10px] uppercase tracking-widest text-[var(--color-text-muted)]">
66 role name
67 </span>
68 <input
69 value={role}
70 onChange={(e) => setRole(e.target.value)}
71 placeholder="admin"
72 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))]"
73 disabled={pending}
74 />
75 </label>
76 <label className="flex flex-[2] flex-col gap-1">
77 <span className="font-mono text-[10px] uppercase tracking-widest text-[var(--color-text-muted)]">
78 permissions (comma-separated)
79 </span>
80 <input
81 value={permissions}
82 onChange={(e) => setPermissions(e.target.value)}
83 placeholder="read, write, *"
84 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))]"
85 disabled={pending}
86 />
87 </label>
88 <button
89 type="submit"
90 disabled={pending || !role.trim()}
91 className="rounded-md px-4 py-2 font-mono text-xs font-medium disabled:opacity-50"
92 style={{ background: '#FFFD74', color: '#111' }}
93 >
94 {pending ? 'saving…' : 'save role'}
95 </button>
96 </div>
97 {err ? (
98 <p className="font-mono text-xs text-red-400">{err}</p>
99 ) : null}
100 {okMsg ? (
101 <p className="font-mono text-xs text-[var(--color-text-muted)]">{okMsg}</p>
102 ) : null}
103 </form>
104 );
105}