@simple-auth-kit/cli 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/package.json +1 -1
- package/registry/admin-apps/nextjs/shared/.husky/commit-msg +1 -0
- package/registry/admin-apps/nextjs/shared/.husky/pre-commit +1 -0
- package/registry/admin-apps/nextjs/shared/.lintstagedrc.json +4 -0
- package/registry/admin-apps/nextjs/shared/.prettierrc.json +3 -0
- package/registry/admin-apps/nextjs/shared/commitlint.config.js +5 -0
- package/registry/admin-apps/nextjs/shared/eslint.config.js +28 -0
- package/registry/admin-apps/nextjs/variants/base/package.json +11 -2
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/audit-log/page.tsx +72 -16
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/edit/page.tsx +107 -26
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/page.tsx +131 -29
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/edit/page.tsx +111 -25
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/page.tsx +97 -22
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/edit/page.tsx +129 -46
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/page.tsx +64 -14
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/[id]/page.tsx +91 -22
- package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/[id]/edit/page.tsx +94 -23
- package/registry/admin-apps/nextjs/variants/workspaces/package.json +11 -2
- package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/account/page.tsx +179 -41
- package/registry/admin-apps/react/shared/.husky/commit-msg +1 -0
- package/registry/admin-apps/react/shared/.husky/pre-commit +1 -0
- package/registry/admin-apps/react/shared/.lintstagedrc.json +4 -0
- package/registry/admin-apps/react/shared/.prettierrc.json +3 -0
- package/registry/admin-apps/react/shared/commitlint.config.js +3 -0
- package/registry/admin-apps/react/shared/eslint.config.js +38 -0
- package/registry/admin-apps/react/variants/base/package.json +15 -2
- package/registry/admin-apps/react/variants/base/src/pages/CountryDetailPage.tsx +76 -19
- package/registry/admin-apps/react/variants/base/src/pages/CustomerDetailPage.tsx +110 -26
- package/registry/admin-apps/react/variants/base/src/pages/EditCountryPage.tsx +115 -29
- package/registry/admin-apps/react/variants/base/src/pages/EditCustomerPage.tsx +123 -31
- package/registry/admin-apps/react/variants/base/src/pages/EditLanguagePage.tsx +100 -24
- package/registry/admin-apps/react/variants/base/src/pages/EditRolePage.tsx +99 -26
- package/registry/admin-apps/react/variants/base/src/pages/EditUserPage.tsx +126 -31
- package/registry/admin-apps/react/variants/base/src/pages/LanguageDetailPage.tsx +75 -19
- package/registry/admin-apps/react/variants/base/src/pages/PermissionDetailPage.tsx +58 -13
- package/registry/admin-apps/react/variants/base/src/pages/PermissionsPage.tsx +132 -29
- package/registry/admin-apps/react/variants/base/src/pages/RoleDetailPage.tsx +70 -17
- package/registry/admin-apps/react/variants/base/src/pages/RolesPage.tsx +277 -59
- package/registry/admin-apps/react/variants/workspaces/package.json +15 -2
- package/registry/combos/express-drizzle/eslint.config.js +18 -0
- package/registry/combos/express-drizzle/package.json +5 -1
- package/registry/combos/express-drizzle/shared/src/request-context.ts +3 -0
- package/registry/combos/express-prisma/eslint.config.js +18 -0
- package/registry/combos/express-prisma/package.json +5 -1
- package/registry/combos/express-prisma/shared/src/request-context.ts +3 -0
- package/registry/combos/nestjs-drizzle/eslint.config.js +18 -0
- package/registry/combos/nestjs-drizzle/package.json +5 -1
- package/registry/combos/nestjs-drizzle/shared/src/auth.controller.ts +138 -33
- package/registry/combos/nestjs-drizzle/shared/src/route-tiers.ts +71 -18
- package/registry/combos/nestjs-prisma/eslint.config.js +18 -0
- package/registry/combos/nestjs-prisma/package.json +5 -1
- package/registry/combos/nestjs-prisma/shared/src/auth.controller.ts +148 -35
- package/registry/combos/nestjs-prisma/shared/src/route-tiers.ts +72 -18
- package/registry/mobile-apps/bare-rn/shared/.husky/commit-msg +1 -0
- package/registry/mobile-apps/bare-rn/shared/.husky/pre-commit +1 -0
- package/registry/mobile-apps/bare-rn/shared/.lintstagedrc.json +4 -0
- package/registry/mobile-apps/bare-rn/shared/.prettierrc.json +5 -0
- package/registry/mobile-apps/bare-rn/shared/commitlint.config.js +3 -0
- package/registry/mobile-apps/bare-rn/shared/eslint.config.js +19 -0
- package/registry/mobile-apps/bare-rn/variants/base/package.json +17 -12
- package/registry/mobile-apps/bare-rn/variants/workspaces/package.json +17 -12
- package/registry/mobile-apps/expo/shared/.husky/commit-msg +1 -0
- package/registry/mobile-apps/expo/shared/.husky/pre-commit +1 -0
- package/registry/mobile-apps/expo/shared/.lintstagedrc.json +4 -0
- package/registry/mobile-apps/expo/shared/.prettierrc.json +5 -0
- package/registry/mobile-apps/expo/shared/commitlint.config.js +3 -0
- package/registry/mobile-apps/expo/shared/eslint.config.js +9 -0
- package/registry/mobile-apps/expo/shared/src/screens/SessionsScreen.tsx +6 -17
- package/registry/mobile-apps/expo/shared/src/store/sessionsStore.ts +35 -0
- package/registry/mobile-apps/expo/variants/base/package.json +12 -3
- package/registry/mobile-apps/expo/variants/workspaces/package.json +12 -3
- package/registry/mobile-apps/expo/variants/workspaces/src/screens/WorkspaceSelectScreen.tsx +1 -1
- package/registry.json +13 -36
- package/simple-auth-kit.ts +13 -10
- package/registry/mobile-apps/bare-rn/shared/.eslintrc.js +0 -4
- package/registry/mobile-apps/bare-rn/shared/.prettierrc.js +0 -5
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type FormEvent,
|
|
3
|
+
startTransition,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
2
8
|
import { Link } from "react-router-dom";
|
|
3
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
PermissionSummary,
|
|
11
|
+
RoleSummary,
|
|
12
|
+
UserSummary,
|
|
13
|
+
} from "@simple-auth-kit/auth-client";
|
|
4
14
|
import { AuthApiError, userIdOf } from "@simple-auth-kit/auth-client";
|
|
5
15
|
import { EyeIcon, PowerIcon } from "lucide-react";
|
|
6
16
|
import { toast } from "sonner";
|
|
@@ -9,13 +19,26 @@ import { authClient } from "@/lib/auth-client";
|
|
|
9
19
|
import { cn } from "@/lib/cn";
|
|
10
20
|
import { AlertModal } from "@/components/alert-modal";
|
|
11
21
|
import { PermissionGroupSelect } from "@/components/permission-group-select";
|
|
12
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
Card,
|
|
24
|
+
CardContent,
|
|
25
|
+
CardDescription,
|
|
26
|
+
CardHeader,
|
|
27
|
+
CardTitle,
|
|
28
|
+
} from "@/components/ui/card";
|
|
13
29
|
import { Input } from "@/components/ui/input";
|
|
14
30
|
import { Label } from "@/components/ui/label";
|
|
15
31
|
import { Button, buttonVariants } from "@/components/ui/button";
|
|
16
32
|
import { Badge } from "@/components/ui/badge";
|
|
17
33
|
import { Modal } from "@/components/ui/modal";
|
|
18
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
Table,
|
|
36
|
+
TableBody,
|
|
37
|
+
TableCell,
|
|
38
|
+
TableHead,
|
|
39
|
+
TableHeader,
|
|
40
|
+
TableRow,
|
|
41
|
+
} from "@/components/ui/table";
|
|
19
42
|
|
|
20
43
|
function apiErrorMessage(err: unknown, fallback: string): string {
|
|
21
44
|
return err instanceof AuthApiError ? err.message : fallback;
|
|
@@ -32,14 +55,22 @@ export function RolesPage() {
|
|
|
32
55
|
const ability = useAbility();
|
|
33
56
|
const canManageRoles = ability.can(PERMISSIONS.rolesManage, "permission");
|
|
34
57
|
const canAssignRoles = ability.can(PERMISSIONS.rolesAssign, "permission");
|
|
35
|
-
const canGrantPermissions = ability.can(
|
|
58
|
+
const canGrantPermissions = ability.can(
|
|
59
|
+
PERMISSIONS.permissionsGrant,
|
|
60
|
+
"permission",
|
|
61
|
+
);
|
|
36
62
|
const canListUsers = ability.can(PERMISSIONS.usersRead, "permission");
|
|
37
|
-
const canReadPermissions = ability.can(
|
|
63
|
+
const canReadPermissions = ability.can(
|
|
64
|
+
PERMISSIONS.permissionsRead,
|
|
65
|
+
"permission",
|
|
66
|
+
);
|
|
38
67
|
|
|
39
68
|
const [roles, setRoles] = useState<RoleSummary[]>([]);
|
|
40
69
|
const [rolesLoading, setRolesLoading] = useState(true);
|
|
41
70
|
const [users, setUsers] = useState<UserSummary[]>([]);
|
|
42
|
-
const [permissionCatalog, setPermissionCatalog] = useState<
|
|
71
|
+
const [permissionCatalog, setPermissionCatalog] = useState<
|
|
72
|
+
PermissionSummary[]
|
|
73
|
+
>([]);
|
|
43
74
|
const [editingRole, setEditingRole] = useState<RoleSummary | null>(null);
|
|
44
75
|
const [deletingRole, setDeletingRole] = useState<RoleSummary | null>(null);
|
|
45
76
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
@@ -59,7 +90,7 @@ export function RolesPage() {
|
|
|
59
90
|
|
|
60
91
|
useEffect(() => {
|
|
61
92
|
if (!canManageRoles) return;
|
|
62
|
-
void loadRoles();
|
|
93
|
+
startTransition(() => void loadRoles());
|
|
63
94
|
}, [canManageRoles, loadRoles]);
|
|
64
95
|
|
|
65
96
|
useEffect(() => {
|
|
@@ -67,7 +98,9 @@ export function RolesPage() {
|
|
|
67
98
|
authClient
|
|
68
99
|
.listUsers({ limit: 100 })
|
|
69
100
|
.then((result) => setUsers(result.items))
|
|
70
|
-
.catch((err) =>
|
|
101
|
+
.catch((err) =>
|
|
102
|
+
toast.error(apiErrorMessage(err, "Couldn't load users.")),
|
|
103
|
+
);
|
|
71
104
|
}, [canListUsers]);
|
|
72
105
|
|
|
73
106
|
useEffect(() => {
|
|
@@ -75,7 +108,11 @@ export function RolesPage() {
|
|
|
75
108
|
authClient
|
|
76
109
|
.listPermissions({ activeOnly: true })
|
|
77
110
|
.then(setPermissionCatalog)
|
|
78
|
-
.catch((err) =>
|
|
111
|
+
.catch((err) =>
|
|
112
|
+
toast.error(
|
|
113
|
+
apiErrorMessage(err, "Couldn't load the permission catalog."),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
79
116
|
}, [canReadPermissions]);
|
|
80
117
|
|
|
81
118
|
function openStatusConfirm(role: RoleSummary) {
|
|
@@ -87,13 +124,23 @@ export function RolesPage() {
|
|
|
87
124
|
if (!pendingRole) return;
|
|
88
125
|
setPendingBusy(true);
|
|
89
126
|
try {
|
|
90
|
-
await authClient.updateRole(pendingRole.id, {
|
|
91
|
-
|
|
92
|
-
|
|
127
|
+
await authClient.updateRole(pendingRole.id, {
|
|
128
|
+
isActive: !pendingRole.isActive,
|
|
129
|
+
});
|
|
130
|
+
setRoles((prev) =>
|
|
131
|
+
prev.map((r) =>
|
|
132
|
+
r.id === pendingRole.id ? { ...r, isActive: !r.isActive } : r,
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
toast.success(
|
|
136
|
+
pendingRole.isActive ? "Role deactivated." : "Role activated.",
|
|
137
|
+
);
|
|
93
138
|
setConfirmOpen(false);
|
|
94
139
|
setPendingRole(null);
|
|
95
140
|
} catch (err) {
|
|
96
|
-
toast.error(
|
|
141
|
+
toast.error(
|
|
142
|
+
apiErrorMessage(err, "Couldn't change this role's status. Try again."),
|
|
143
|
+
);
|
|
97
144
|
} finally {
|
|
98
145
|
setPendingBusy(false);
|
|
99
146
|
}
|
|
@@ -103,7 +150,10 @@ export function RolesPage() {
|
|
|
103
150
|
<div className="flex flex-col gap-6">
|
|
104
151
|
<div>
|
|
105
152
|
<h1 className="text-2xl font-semibold">Roles & permissions</h1>
|
|
106
|
-
<p className="text-sm text-muted-foreground">
|
|
153
|
+
<p className="text-sm text-muted-foreground">
|
|
154
|
+
Create roles, attach permissions to them, and manage direct grants on
|
|
155
|
+
individual users.
|
|
156
|
+
</p>
|
|
107
157
|
</div>
|
|
108
158
|
|
|
109
159
|
<AlertModal
|
|
@@ -122,13 +172,17 @@ export function RolesPage() {
|
|
|
122
172
|
<Card>
|
|
123
173
|
<CardHeader>
|
|
124
174
|
<CardTitle className="text-base">Roles</CardTitle>
|
|
125
|
-
<CardDescription>
|
|
175
|
+
<CardDescription>
|
|
176
|
+
Every role defined on this deployment.
|
|
177
|
+
</CardDescription>
|
|
126
178
|
</CardHeader>
|
|
127
179
|
<CardContent>
|
|
128
180
|
{rolesLoading ? (
|
|
129
181
|
<p className="text-sm text-muted-foreground">Loading…</p>
|
|
130
182
|
) : roles.length === 0 ? (
|
|
131
|
-
<p className="text-sm text-muted-foreground">
|
|
183
|
+
<p className="text-sm text-muted-foreground">
|
|
184
|
+
No roles yet. Create one below.
|
|
185
|
+
</p>
|
|
132
186
|
) : (
|
|
133
187
|
<Table>
|
|
134
188
|
<TableHeader>
|
|
@@ -145,23 +199,39 @@ export function RolesPage() {
|
|
|
145
199
|
<TableBody>
|
|
146
200
|
{roles.map((role) => (
|
|
147
201
|
<TableRow key={role.id}>
|
|
148
|
-
<TableCell className="font-mono text-xs">
|
|
202
|
+
<TableCell className="font-mono text-xs">
|
|
203
|
+
{role.slug}
|
|
204
|
+
</TableCell>
|
|
149
205
|
<TableCell className="font-medium">{role.name}</TableCell>
|
|
150
206
|
<TableCell>{role.displayName}</TableCell>
|
|
151
207
|
<TableCell>
|
|
152
|
-
{role.isDefault ?
|
|
208
|
+
{role.isDefault ? (
|
|
209
|
+
<Badge variant="secondary">Default</Badge>
|
|
210
|
+
) : (
|
|
211
|
+
<span className="text-muted-foreground">—</span>
|
|
212
|
+
)}
|
|
153
213
|
</TableCell>
|
|
154
214
|
<TableCell>
|
|
155
|
-
<Badge
|
|
215
|
+
<Badge
|
|
216
|
+
variant={role.isActive ? "success" : "destructive"}
|
|
217
|
+
>
|
|
218
|
+
{role.isActive ? "Active" : "Inactive"}
|
|
219
|
+
</Badge>
|
|
156
220
|
</TableCell>
|
|
157
221
|
<TableCell className="text-xs text-muted-foreground">
|
|
158
|
-
{role.permissions.length} permission
|
|
222
|
+
{role.permissions.length} permission
|
|
223
|
+
{role.permissions.length === 1 ? "" : "s"}
|
|
159
224
|
</TableCell>
|
|
160
225
|
<TableCell className="text-right">
|
|
161
226
|
<div className="flex justify-end gap-2">
|
|
162
227
|
<Link
|
|
163
228
|
to={`/roles/${role.id}`}
|
|
164
|
-
className={cn(
|
|
229
|
+
className={cn(
|
|
230
|
+
buttonVariants({
|
|
231
|
+
variant: "outline",
|
|
232
|
+
size: "icon",
|
|
233
|
+
}),
|
|
234
|
+
)}
|
|
165
235
|
title="View details"
|
|
166
236
|
>
|
|
167
237
|
<EyeIcon />
|
|
@@ -170,15 +240,29 @@ export function RolesPage() {
|
|
|
170
240
|
size="icon"
|
|
171
241
|
variant="outline"
|
|
172
242
|
disabled={!canManageRoles}
|
|
173
|
-
title={
|
|
243
|
+
title={
|
|
244
|
+
canManageRoles
|
|
245
|
+
? role.isActive
|
|
246
|
+
? "Deactivate this role"
|
|
247
|
+
: "Activate this role"
|
|
248
|
+
: undefined
|
|
249
|
+
}
|
|
174
250
|
onClick={() => openStatusConfirm(role)}
|
|
175
251
|
>
|
|
176
252
|
<PowerIcon />
|
|
177
253
|
</Button>
|
|
178
|
-
<Button
|
|
254
|
+
<Button
|
|
255
|
+
size="sm"
|
|
256
|
+
variant="outline"
|
|
257
|
+
onClick={() => setEditingRole(role)}
|
|
258
|
+
>
|
|
179
259
|
Edit
|
|
180
260
|
</Button>
|
|
181
|
-
<Button
|
|
261
|
+
<Button
|
|
262
|
+
size="sm"
|
|
263
|
+
variant="destructive"
|
|
264
|
+
onClick={() => setDeletingRole(role)}
|
|
265
|
+
>
|
|
182
266
|
Delete
|
|
183
267
|
</Button>
|
|
184
268
|
</div>
|
|
@@ -246,9 +330,17 @@ function CreateRoleCard({
|
|
|
246
330
|
event.preventDefault();
|
|
247
331
|
setSubmitting(true);
|
|
248
332
|
try {
|
|
249
|
-
const role = await authClient.createRole({
|
|
333
|
+
const role = await authClient.createRole({
|
|
334
|
+
slug,
|
|
335
|
+
displayName: displayName || undefined,
|
|
336
|
+
description: description || undefined,
|
|
337
|
+
});
|
|
250
338
|
if (selectedPermissions.length > 0) {
|
|
251
|
-
await Promise.all(
|
|
339
|
+
await Promise.all(
|
|
340
|
+
selectedPermissions.map((permSlug) =>
|
|
341
|
+
authClient.attachPermissionToRole(role.id, permSlug),
|
|
342
|
+
),
|
|
343
|
+
);
|
|
252
344
|
}
|
|
253
345
|
onCreated({ ...role, permissions: selectedPermissions });
|
|
254
346
|
toast.success(`Role "${role.name}" created.`);
|
|
@@ -257,7 +349,9 @@ function CreateRoleCard({
|
|
|
257
349
|
setDescription("");
|
|
258
350
|
setSelectedPermissions([]);
|
|
259
351
|
} catch (err) {
|
|
260
|
-
toast.error(
|
|
352
|
+
toast.error(
|
|
353
|
+
apiErrorMessage(err, "Couldn't create the role. Try a different slug."),
|
|
354
|
+
);
|
|
261
355
|
} finally {
|
|
262
356
|
setSubmitting(false);
|
|
263
357
|
}
|
|
@@ -267,26 +361,48 @@ function CreateRoleCard({
|
|
|
267
361
|
<Card>
|
|
268
362
|
<CardHeader>
|
|
269
363
|
<CardTitle className="text-base">Create role</CardTitle>
|
|
270
|
-
<CardDescription>
|
|
364
|
+
<CardDescription>
|
|
365
|
+
Pick the permissions this role grants right here — you can still
|
|
366
|
+
change them later.
|
|
367
|
+
</CardDescription>
|
|
271
368
|
</CardHeader>
|
|
272
369
|
<CardContent>
|
|
273
370
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
|
274
371
|
<div className="flex flex-col gap-1.5">
|
|
275
372
|
<Label htmlFor="role-slug">Role slug</Label>
|
|
276
|
-
<Input
|
|
373
|
+
<Input
|
|
374
|
+
id="role-slug"
|
|
375
|
+
value={slug}
|
|
376
|
+
onChange={(e) => setSlug(e.target.value)}
|
|
377
|
+
placeholder="e.g. auditor"
|
|
378
|
+
required
|
|
379
|
+
/>
|
|
277
380
|
</div>
|
|
278
381
|
<div className="flex flex-col gap-1.5">
|
|
279
382
|
<Label htmlFor="role-displayName">Display name</Label>
|
|
280
|
-
<Input
|
|
383
|
+
<Input
|
|
384
|
+
id="role-displayName"
|
|
385
|
+
value={displayName}
|
|
386
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
387
|
+
placeholder="e.g. Auditor"
|
|
388
|
+
/>
|
|
281
389
|
</div>
|
|
282
390
|
<div className="flex flex-col gap-1.5">
|
|
283
391
|
<Label htmlFor="role-description">Description</Label>
|
|
284
|
-
<Input
|
|
392
|
+
<Input
|
|
393
|
+
id="role-description"
|
|
394
|
+
value={description}
|
|
395
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
396
|
+
/>
|
|
285
397
|
</div>
|
|
286
398
|
{canReadPermissions ? (
|
|
287
399
|
<div className="flex flex-col gap-2">
|
|
288
400
|
<Label>Permissions</Label>
|
|
289
|
-
<PermissionGroupSelect
|
|
401
|
+
<PermissionGroupSelect
|
|
402
|
+
permissions={permissionCatalog}
|
|
403
|
+
selected={selectedPermissions}
|
|
404
|
+
onChange={setSelectedPermissions}
|
|
405
|
+
/>
|
|
290
406
|
</div>
|
|
291
407
|
) : null}
|
|
292
408
|
<Button type="submit" disabled={submitting} className="self-start">
|
|
@@ -341,16 +457,24 @@ export function EditRoleDialog({
|
|
|
341
457
|
if (!role) return;
|
|
342
458
|
setSubmitting(true);
|
|
343
459
|
try {
|
|
344
|
-
const toAttach = selectedPermissions.filter(
|
|
345
|
-
|
|
460
|
+
const toAttach = selectedPermissions.filter(
|
|
461
|
+
(slug) => !role.permissions.includes(slug),
|
|
462
|
+
);
|
|
463
|
+
const toDetach = role.permissions.filter(
|
|
464
|
+
(slug) => !selectedPermissions.includes(slug),
|
|
465
|
+
);
|
|
346
466
|
const [updated] = await Promise.all([
|
|
347
467
|
authClient.updateRole(role.id, {
|
|
348
468
|
displayName: displayName.trim() === "" ? undefined : displayName,
|
|
349
469
|
description: description.trim() === "" ? undefined : description,
|
|
350
470
|
isActive,
|
|
351
471
|
}),
|
|
352
|
-
...toAttach.map((slug) =>
|
|
353
|
-
|
|
472
|
+
...toAttach.map((slug) =>
|
|
473
|
+
authClient.attachPermissionToRole(role.id, slug),
|
|
474
|
+
),
|
|
475
|
+
...toDetach.map((slug) =>
|
|
476
|
+
authClient.detachPermissionFromRole(role.id, slug),
|
|
477
|
+
),
|
|
354
478
|
]);
|
|
355
479
|
toast.success(`Role "${updated.name}" updated.`);
|
|
356
480
|
onSaved({ ...updated, permissions: selectedPermissions });
|
|
@@ -367,11 +491,20 @@ export function EditRoleDialog({
|
|
|
367
491
|
}
|
|
368
492
|
|
|
369
493
|
return (
|
|
370
|
-
<Modal
|
|
494
|
+
<Modal
|
|
495
|
+
isOpen={role !== null}
|
|
496
|
+
onClose={handleClose}
|
|
497
|
+
title="Edit role"
|
|
498
|
+
description={role ? `Updates the "${role.slug}" role.` : ""}
|
|
499
|
+
>
|
|
371
500
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
372
501
|
<div className="flex flex-col gap-1.5">
|
|
373
502
|
<Label htmlFor="edit-role-displayName">Display name</Label>
|
|
374
|
-
<Input
|
|
503
|
+
<Input
|
|
504
|
+
id="edit-role-displayName"
|
|
505
|
+
value={displayName}
|
|
506
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
507
|
+
/>
|
|
375
508
|
</div>
|
|
376
509
|
<div className="flex flex-col gap-1.5">
|
|
377
510
|
<Label htmlFor="edit-role-description">Description</Label>
|
|
@@ -395,7 +528,11 @@ export function EditRoleDialog({
|
|
|
395
528
|
{canReadPermissions ? (
|
|
396
529
|
<div className="flex flex-col gap-2">
|
|
397
530
|
<Label>Permissions</Label>
|
|
398
|
-
<PermissionGroupSelect
|
|
531
|
+
<PermissionGroupSelect
|
|
532
|
+
permissions={permissionCatalog}
|
|
533
|
+
selected={selectedPermissions}
|
|
534
|
+
onChange={setSelectedPermissions}
|
|
535
|
+
/>
|
|
399
536
|
</div>
|
|
400
537
|
) : null}
|
|
401
538
|
<div className="flex gap-2">
|
|
@@ -441,11 +578,19 @@ export function DeleteRoleDialog({
|
|
|
441
578
|
isOpen={role !== null}
|
|
442
579
|
onClose={onClose}
|
|
443
580
|
title="Delete role"
|
|
444
|
-
description={
|
|
581
|
+
description={
|
|
582
|
+
role
|
|
583
|
+
? `"${role.slug}" stops being resolved for anyone who holds it. Existing assignments are left in place.`
|
|
584
|
+
: ""
|
|
585
|
+
}
|
|
445
586
|
>
|
|
446
587
|
<div className="flex flex-col gap-4">
|
|
447
588
|
<div className="flex gap-2">
|
|
448
|
-
<Button
|
|
589
|
+
<Button
|
|
590
|
+
variant="destructive"
|
|
591
|
+
disabled={submitting}
|
|
592
|
+
onClick={() => void handleDelete()}
|
|
593
|
+
>
|
|
449
594
|
{submitting ? "Deleting…" : "Delete role"}
|
|
450
595
|
</Button>
|
|
451
596
|
<Button variant="outline" onClick={onClose}>
|
|
@@ -460,7 +605,9 @@ export function DeleteRoleDialog({
|
|
|
460
605
|
function AssignRoleCard({ users }: { users: UserSummary[] }) {
|
|
461
606
|
const [userId, setUserId] = useState("");
|
|
462
607
|
const [role, setRole] = useState("");
|
|
463
|
-
const [submitting, setSubmitting] = useState<"assign" | "revoke" | null>(
|
|
608
|
+
const [submitting, setSubmitting] = useState<"assign" | "revoke" | null>(
|
|
609
|
+
null,
|
|
610
|
+
);
|
|
464
611
|
|
|
465
612
|
async function handleAssign(event: FormEvent) {
|
|
466
613
|
event.preventDefault();
|
|
@@ -469,7 +616,12 @@ function AssignRoleCard({ users }: { users: UserSummary[] }) {
|
|
|
469
616
|
await authClient.assignRole(userId, role);
|
|
470
617
|
toast.success(`Role "${role}" assigned.`);
|
|
471
618
|
} catch (err) {
|
|
472
|
-
toast.error(
|
|
619
|
+
toast.error(
|
|
620
|
+
apiErrorMessage(
|
|
621
|
+
err,
|
|
622
|
+
"Couldn't assign the role. Check the user and role name.",
|
|
623
|
+
),
|
|
624
|
+
);
|
|
473
625
|
} finally {
|
|
474
626
|
setSubmitting(null);
|
|
475
627
|
}
|
|
@@ -481,7 +633,12 @@ function AssignRoleCard({ users }: { users: UserSummary[] }) {
|
|
|
481
633
|
await authClient.revokeRole(userId, role);
|
|
482
634
|
toast.success(`Role "${role}" revoked.`);
|
|
483
635
|
} catch (err) {
|
|
484
|
-
toast.error(
|
|
636
|
+
toast.error(
|
|
637
|
+
apiErrorMessage(
|
|
638
|
+
err,
|
|
639
|
+
"Couldn't revoke the role. Check the user and role name.",
|
|
640
|
+
),
|
|
641
|
+
);
|
|
485
642
|
} finally {
|
|
486
643
|
setSubmitting(null);
|
|
487
644
|
}
|
|
@@ -491,20 +648,44 @@ function AssignRoleCard({ users }: { users: UserSummary[] }) {
|
|
|
491
648
|
<Card>
|
|
492
649
|
<CardHeader>
|
|
493
650
|
<CardTitle className="text-base">Assign or revoke a role</CardTitle>
|
|
494
|
-
<CardDescription>
|
|
651
|
+
<CardDescription>
|
|
652
|
+
Roles union additively; a new role takes effect the next time that
|
|
653
|
+
user signs in.
|
|
654
|
+
</CardDescription>
|
|
495
655
|
</CardHeader>
|
|
496
656
|
<CardContent>
|
|
497
657
|
<form onSubmit={handleAssign} className="flex flex-col gap-3">
|
|
498
|
-
<UserPicker
|
|
658
|
+
<UserPicker
|
|
659
|
+
users={users}
|
|
660
|
+
value={userId}
|
|
661
|
+
onChange={setUserId}
|
|
662
|
+
idPrefix="assign"
|
|
663
|
+
/>
|
|
499
664
|
<div className="flex flex-col gap-1.5">
|
|
500
665
|
<Label htmlFor="assign-role-name">Role name</Label>
|
|
501
|
-
<Input
|
|
666
|
+
<Input
|
|
667
|
+
id="assign-role-name"
|
|
668
|
+
value={role}
|
|
669
|
+
onChange={(e) => setRole(e.target.value)}
|
|
670
|
+
placeholder="e.g. auditor"
|
|
671
|
+
required
|
|
672
|
+
/>
|
|
502
673
|
</div>
|
|
503
674
|
<div className="flex gap-2">
|
|
504
|
-
<Button
|
|
675
|
+
<Button
|
|
676
|
+
type="submit"
|
|
677
|
+
disabled={submitting !== null}
|
|
678
|
+
className="self-start"
|
|
679
|
+
>
|
|
505
680
|
{submitting === "assign" ? "Assigning…" : "Assign role"}
|
|
506
681
|
</Button>
|
|
507
|
-
<Button
|
|
682
|
+
<Button
|
|
683
|
+
type="button"
|
|
684
|
+
variant="outline"
|
|
685
|
+
disabled={submitting !== null}
|
|
686
|
+
onClick={() => void handleRevoke()}
|
|
687
|
+
className="self-start"
|
|
688
|
+
>
|
|
508
689
|
{submitting === "revoke" ? "Revoking…" : "Revoke role"}
|
|
509
690
|
</Button>
|
|
510
691
|
</div>
|
|
@@ -526,7 +707,12 @@ function DirectPermissionCard({ users }: { users: UserSummary[] }) {
|
|
|
526
707
|
await authClient.grantPermission(userId, permission);
|
|
527
708
|
toast.success(`Permission "${permission}" granted.`);
|
|
528
709
|
} catch (err) {
|
|
529
|
-
toast.error(
|
|
710
|
+
toast.error(
|
|
711
|
+
apiErrorMessage(
|
|
712
|
+
err,
|
|
713
|
+
"Couldn't grant the permission. Check the user and permission key.",
|
|
714
|
+
),
|
|
715
|
+
);
|
|
530
716
|
} finally {
|
|
531
717
|
setSubmitting(null);
|
|
532
718
|
}
|
|
@@ -538,7 +724,12 @@ function DirectPermissionCard({ users }: { users: UserSummary[] }) {
|
|
|
538
724
|
await authClient.revokePermission(userId, permission);
|
|
539
725
|
toast.success(`Permission "${permission}" revoked.`);
|
|
540
726
|
} catch (err) {
|
|
541
|
-
toast.error(
|
|
727
|
+
toast.error(
|
|
728
|
+
apiErrorMessage(
|
|
729
|
+
err,
|
|
730
|
+
"Couldn't revoke the permission. Check the user and permission key.",
|
|
731
|
+
),
|
|
732
|
+
);
|
|
542
733
|
} finally {
|
|
543
734
|
setSubmitting(null);
|
|
544
735
|
}
|
|
@@ -547,12 +738,22 @@ function DirectPermissionCard({ users }: { users: UserSummary[] }) {
|
|
|
547
738
|
return (
|
|
548
739
|
<Card>
|
|
549
740
|
<CardHeader>
|
|
550
|
-
<CardTitle className="text-base">
|
|
551
|
-
|
|
741
|
+
<CardTitle className="text-base">
|
|
742
|
+
Grant or revoke a permission directly
|
|
743
|
+
</CardTitle>
|
|
744
|
+
<CardDescription>
|
|
745
|
+
Bypasses roles entirely — additive to whatever their roles already
|
|
746
|
+
grant.
|
|
747
|
+
</CardDescription>
|
|
552
748
|
</CardHeader>
|
|
553
749
|
<CardContent>
|
|
554
750
|
<form onSubmit={handleGrant} className="flex flex-col gap-3">
|
|
555
|
-
<UserPicker
|
|
751
|
+
<UserPicker
|
|
752
|
+
users={users}
|
|
753
|
+
value={userId}
|
|
754
|
+
onChange={setUserId}
|
|
755
|
+
idPrefix="direct"
|
|
756
|
+
/>
|
|
556
757
|
<div className="flex flex-col gap-1.5">
|
|
557
758
|
<Label htmlFor="direct-permission-key">Permission key</Label>
|
|
558
759
|
<Input
|
|
@@ -562,13 +763,25 @@ function DirectPermissionCard({ users }: { users: UserSummary[] }) {
|
|
|
562
763
|
placeholder="e.g. users:read"
|
|
563
764
|
required
|
|
564
765
|
/>
|
|
565
|
-
<p className="text-xs text-muted-foreground">
|
|
766
|
+
<p className="text-xs text-muted-foreground">
|
|
767
|
+
Catalog: {CATALOG_HINT}
|
|
768
|
+
</p>
|
|
566
769
|
</div>
|
|
567
770
|
<div className="flex gap-2">
|
|
568
|
-
<Button
|
|
771
|
+
<Button
|
|
772
|
+
type="submit"
|
|
773
|
+
disabled={submitting !== null}
|
|
774
|
+
className="self-start"
|
|
775
|
+
>
|
|
569
776
|
{submitting === "grant" ? "Granting…" : "Grant permission"}
|
|
570
777
|
</Button>
|
|
571
|
-
<Button
|
|
778
|
+
<Button
|
|
779
|
+
type="button"
|
|
780
|
+
variant="outline"
|
|
781
|
+
disabled={submitting !== null}
|
|
782
|
+
onClick={() => void handleRevoke()}
|
|
783
|
+
className="self-start"
|
|
784
|
+
>
|
|
572
785
|
{submitting === "revoke" ? "Revoking…" : "Revoke permission"}
|
|
573
786
|
</Button>
|
|
574
787
|
</div>
|
|
@@ -607,7 +820,12 @@ function UserPicker({
|
|
|
607
820
|
))}
|
|
608
821
|
</select>
|
|
609
822
|
) : null}
|
|
610
|
-
<Input
|
|
823
|
+
<Input
|
|
824
|
+
placeholder="or paste a user ID"
|
|
825
|
+
value={value}
|
|
826
|
+
onChange={(e) => onChange(e.target.value)}
|
|
827
|
+
required
|
|
828
|
+
/>
|
|
611
829
|
</div>
|
|
612
830
|
);
|
|
613
831
|
}
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"dev": "vite",
|
|
9
9
|
"build": "tsc --noEmit && vite build",
|
|
10
10
|
"preview": "vite preview",
|
|
11
|
-
"typecheck": "tsc --noEmit"
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"prepare": "husky"
|
|
12
14
|
},
|
|
13
15
|
"dependencies": {
|
|
14
16
|
"@casl/ability": "^6.7.3",
|
|
@@ -48,14 +50,25 @@
|
|
|
48
50
|
"vaul": "^1.1.2"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
53
|
+
"@commitlint/cli": "^21.2.2",
|
|
54
|
+
"@commitlint/config-conventional": "^21.2.2",
|
|
55
|
+
"@eslint/js": "^9.39.5",
|
|
51
56
|
"@tailwindcss/postcss": "^4.3.3",
|
|
52
57
|
"@types/node": "^26.2.0",
|
|
53
58
|
"@types/react": "^19.2.7",
|
|
54
59
|
"@types/react-dom": "^19.2.4",
|
|
55
60
|
"@vitejs/plugin-react": "^5.0.4",
|
|
61
|
+
"eslint": "^9.39.5",
|
|
62
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
63
|
+
"eslint-plugin-react-refresh": "^0.5.6",
|
|
64
|
+
"globals": "^17.12.0",
|
|
65
|
+
"husky": "^9.1.7",
|
|
66
|
+
"lint-staged": "^17.5.1",
|
|
56
67
|
"postcss": "^8.5.6",
|
|
68
|
+
"prettier": "^3.9.6",
|
|
57
69
|
"tailwindcss": "^4.3.3",
|
|
58
|
-
"typescript": "^
|
|
70
|
+
"typescript": "^6.0.3",
|
|
71
|
+
"typescript-eslint": "^8.70.0",
|
|
59
72
|
"vite": "^7.1.14"
|
|
60
73
|
}
|
|
61
74
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import tseslint from "typescript-eslint";
|
|
3
|
+
|
|
4
|
+
export default tseslint.config(
|
|
5
|
+
{ ignores: [".variant/**", "node_modules/**", "dist/**", "generated/**"] },
|
|
6
|
+
js.configs.recommended,
|
|
7
|
+
...tseslint.configs.recommended,
|
|
8
|
+
{
|
|
9
|
+
rules: {
|
|
10
|
+
// This combo's leading-underscore convention (e.g. an interface-required parameter that's
|
|
11
|
+
// deliberately unused) predates this eslint config — respect it rather than flag it.
|
|
12
|
+
"@typescript-eslint/no-unused-vars": [
|
|
13
|
+
"error",
|
|
14
|
+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
);
|