@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.
Files changed (77) hide show
  1. package/README.md +6 -5
  2. package/package.json +1 -1
  3. package/registry/admin-apps/nextjs/shared/.husky/commit-msg +1 -0
  4. package/registry/admin-apps/nextjs/shared/.husky/pre-commit +1 -0
  5. package/registry/admin-apps/nextjs/shared/.lintstagedrc.json +4 -0
  6. package/registry/admin-apps/nextjs/shared/.prettierrc.json +3 -0
  7. package/registry/admin-apps/nextjs/shared/commitlint.config.js +5 -0
  8. package/registry/admin-apps/nextjs/shared/eslint.config.js +28 -0
  9. package/registry/admin-apps/nextjs/variants/base/package.json +11 -2
  10. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/audit-log/page.tsx +72 -16
  11. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/edit/page.tsx +107 -26
  12. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/page.tsx +131 -29
  13. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/edit/page.tsx +111 -25
  14. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/page.tsx +97 -22
  15. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/edit/page.tsx +129 -46
  16. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/page.tsx +64 -14
  17. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/[id]/page.tsx +91 -22
  18. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/[id]/edit/page.tsx +94 -23
  19. package/registry/admin-apps/nextjs/variants/workspaces/package.json +11 -2
  20. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/account/page.tsx +179 -41
  21. package/registry/admin-apps/react/shared/.husky/commit-msg +1 -0
  22. package/registry/admin-apps/react/shared/.husky/pre-commit +1 -0
  23. package/registry/admin-apps/react/shared/.lintstagedrc.json +4 -0
  24. package/registry/admin-apps/react/shared/.prettierrc.json +3 -0
  25. package/registry/admin-apps/react/shared/commitlint.config.js +3 -0
  26. package/registry/admin-apps/react/shared/eslint.config.js +38 -0
  27. package/registry/admin-apps/react/variants/base/package.json +15 -2
  28. package/registry/admin-apps/react/variants/base/src/pages/CountryDetailPage.tsx +76 -19
  29. package/registry/admin-apps/react/variants/base/src/pages/CustomerDetailPage.tsx +110 -26
  30. package/registry/admin-apps/react/variants/base/src/pages/EditCountryPage.tsx +115 -29
  31. package/registry/admin-apps/react/variants/base/src/pages/EditCustomerPage.tsx +123 -31
  32. package/registry/admin-apps/react/variants/base/src/pages/EditLanguagePage.tsx +100 -24
  33. package/registry/admin-apps/react/variants/base/src/pages/EditRolePage.tsx +99 -26
  34. package/registry/admin-apps/react/variants/base/src/pages/EditUserPage.tsx +126 -31
  35. package/registry/admin-apps/react/variants/base/src/pages/LanguageDetailPage.tsx +75 -19
  36. package/registry/admin-apps/react/variants/base/src/pages/PermissionDetailPage.tsx +58 -13
  37. package/registry/admin-apps/react/variants/base/src/pages/PermissionsPage.tsx +132 -29
  38. package/registry/admin-apps/react/variants/base/src/pages/RoleDetailPage.tsx +70 -17
  39. package/registry/admin-apps/react/variants/base/src/pages/RolesPage.tsx +277 -59
  40. package/registry/admin-apps/react/variants/workspaces/package.json +15 -2
  41. package/registry/combos/express-drizzle/eslint.config.js +18 -0
  42. package/registry/combos/express-drizzle/package.json +5 -1
  43. package/registry/combos/express-drizzle/shared/src/request-context.ts +3 -0
  44. package/registry/combos/express-prisma/eslint.config.js +18 -0
  45. package/registry/combos/express-prisma/package.json +5 -1
  46. package/registry/combos/express-prisma/shared/src/request-context.ts +3 -0
  47. package/registry/combos/nestjs-drizzle/eslint.config.js +18 -0
  48. package/registry/combos/nestjs-drizzle/package.json +5 -1
  49. package/registry/combos/nestjs-drizzle/shared/src/auth.controller.ts +138 -33
  50. package/registry/combos/nestjs-drizzle/shared/src/route-tiers.ts +71 -18
  51. package/registry/combos/nestjs-prisma/eslint.config.js +18 -0
  52. package/registry/combos/nestjs-prisma/package.json +5 -1
  53. package/registry/combos/nestjs-prisma/shared/src/auth.controller.ts +148 -35
  54. package/registry/combos/nestjs-prisma/shared/src/route-tiers.ts +72 -18
  55. package/registry/mobile-apps/bare-rn/shared/.husky/commit-msg +1 -0
  56. package/registry/mobile-apps/bare-rn/shared/.husky/pre-commit +1 -0
  57. package/registry/mobile-apps/bare-rn/shared/.lintstagedrc.json +4 -0
  58. package/registry/mobile-apps/bare-rn/shared/.prettierrc.json +5 -0
  59. package/registry/mobile-apps/bare-rn/shared/commitlint.config.js +3 -0
  60. package/registry/mobile-apps/bare-rn/shared/eslint.config.js +19 -0
  61. package/registry/mobile-apps/bare-rn/variants/base/package.json +17 -12
  62. package/registry/mobile-apps/bare-rn/variants/workspaces/package.json +17 -12
  63. package/registry/mobile-apps/expo/shared/.husky/commit-msg +1 -0
  64. package/registry/mobile-apps/expo/shared/.husky/pre-commit +1 -0
  65. package/registry/mobile-apps/expo/shared/.lintstagedrc.json +4 -0
  66. package/registry/mobile-apps/expo/shared/.prettierrc.json +5 -0
  67. package/registry/mobile-apps/expo/shared/commitlint.config.js +3 -0
  68. package/registry/mobile-apps/expo/shared/eslint.config.js +9 -0
  69. package/registry/mobile-apps/expo/shared/src/screens/SessionsScreen.tsx +6 -17
  70. package/registry/mobile-apps/expo/shared/src/store/sessionsStore.ts +35 -0
  71. package/registry/mobile-apps/expo/variants/base/package.json +12 -3
  72. package/registry/mobile-apps/expo/variants/workspaces/package.json +12 -3
  73. package/registry/mobile-apps/expo/variants/workspaces/src/screens/WorkspaceSelectScreen.tsx +1 -1
  74. package/registry.json +13 -36
  75. package/simple-auth-kit.ts +13 -10
  76. package/registry/mobile-apps/bare-rn/shared/.eslintrc.js +0 -4
  77. package/registry/mobile-apps/bare-rn/shared/.prettierrc.js +0 -5
@@ -1,6 +1,16 @@
1
- import { type FormEvent, useCallback, useEffect, useState } from "react";
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 { PermissionSummary, RoleSummary, UserSummary } from "@simple-auth-kit/auth-client";
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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
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(PERMISSIONS.permissionsGrant, "permission");
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(PERMISSIONS.permissionsRead, "permission");
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<PermissionSummary[]>([]);
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) => toast.error(apiErrorMessage(err, "Couldn't load users.")));
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) => toast.error(apiErrorMessage(err, "Couldn't load the permission catalog.")));
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, { isActive: !pendingRole.isActive });
91
- setRoles((prev) => prev.map((r) => (r.id === pendingRole.id ? { ...r, isActive: !r.isActive } : r)));
92
- toast.success(pendingRole.isActive ? "Role deactivated." : "Role activated.");
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(apiErrorMessage(err, "Couldn't change this role's status. Try again."));
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">Create roles, attach permissions to them, and manage direct grants on individual users.</p>
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>Every role defined on this deployment.</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">No roles yet. Create one below.</p>
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">{role.slug}</TableCell>
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 ? <Badge variant="secondary">Default</Badge> : <span className="text-muted-foreground">—</span>}
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 variant={role.isActive ? "success" : "destructive"}>{role.isActive ? "Active" : "Inactive"}</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{role.permissions.length === 1 ? "" : "s"}
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(buttonVariants({ variant: "outline", size: "icon" }))}
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={canManageRoles ? (role.isActive ? "Deactivate this role" : "Activate this role") : undefined}
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 size="sm" variant="outline" onClick={() => setEditingRole(role)}>
254
+ <Button
255
+ size="sm"
256
+ variant="outline"
257
+ onClick={() => setEditingRole(role)}
258
+ >
179
259
  Edit
180
260
  </Button>
181
- <Button size="sm" variant="destructive" onClick={() => setDeletingRole(role)}>
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({ slug, displayName: displayName || undefined, description: description || undefined });
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(selectedPermissions.map((permSlug) => authClient.attachPermissionToRole(role.id, permSlug)));
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(apiErrorMessage(err, "Couldn't create the role. Try a different slug."));
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>Pick the permissions this role grants right here — you can still change them later.</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 id="role-slug" value={slug} onChange={(e) => setSlug(e.target.value)} placeholder="e.g. auditor" required />
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 id="role-displayName" value={displayName} onChange={(e) => setDisplayName(e.target.value)} placeholder="e.g. Auditor" />
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 id="role-description" value={description} onChange={(e) => setDescription(e.target.value)} />
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 permissions={permissionCatalog} selected={selectedPermissions} onChange={setSelectedPermissions} />
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((slug) => !role.permissions.includes(slug));
345
- const toDetach = role.permissions.filter((slug) => !selectedPermissions.includes(slug));
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) => authClient.attachPermissionToRole(role.id, slug)),
353
- ...toDetach.map((slug) => authClient.detachPermissionFromRole(role.id, slug)),
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 isOpen={role !== null} onClose={handleClose} title="Edit role" description={role ? `Updates the "${role.slug}" role.` : ""}>
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 id="edit-role-displayName" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
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 permissions={permissionCatalog} selected={selectedPermissions} onChange={setSelectedPermissions} />
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={role ? `"${role.slug}" stops being resolved for anyone who holds it. Existing assignments are left in place.` : ""}
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 variant="destructive" disabled={submitting} onClick={() => void handleDelete()}>
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>(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(apiErrorMessage(err, "Couldn't assign the role. Check the user and role name."));
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(apiErrorMessage(err, "Couldn't revoke the role. Check the user and role name."));
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>Roles union additively; a new role takes effect the next time that user signs in.</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 users={users} value={userId} onChange={setUserId} idPrefix="assign" />
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 id="assign-role-name" value={role} onChange={(e) => setRole(e.target.value)} placeholder="e.g. auditor" required />
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 type="submit" disabled={submitting !== null} className="self-start">
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 type="button" variant="outline" disabled={submitting !== null} onClick={() => void handleRevoke()} className="self-start">
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(apiErrorMessage(err, "Couldn't grant the permission. Check the user and permission key."));
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(apiErrorMessage(err, "Couldn't revoke the permission. Check the user and permission key."));
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">Grant or revoke a permission directly</CardTitle>
551
- <CardDescription>Bypasses roles entirely additive to whatever their roles already grant.</CardDescription>
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 users={users} value={userId} onChange={setUserId} idPrefix="direct" />
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">Catalog: {CATALOG_HINT}</p>
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 type="submit" disabled={submitting !== null} className="self-start">
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 type="button" variant="outline" disabled={submitting !== null} onClick={() => void handleRevoke()} className="self-start">
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 placeholder="or paste a user ID" value={value} onChange={(e) => onChange(e.target.value)} required />
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": "^7.0.2",
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
+ );