@simple-auth-kit/cli 1.3.2 → 1.4.1

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 (80) hide show
  1. package/lib/copy.ts +67 -10
  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/shared/gitignore +11 -0
  10. package/registry/admin-apps/nextjs/variants/base/package.json +11 -2
  11. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/audit-log/page.tsx +72 -16
  12. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/edit/page.tsx +107 -26
  13. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/page.tsx +131 -29
  14. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/edit/page.tsx +111 -25
  15. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/page.tsx +97 -22
  16. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/edit/page.tsx +129 -46
  17. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/page.tsx +64 -14
  18. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/[id]/page.tsx +91 -22
  19. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/[id]/edit/page.tsx +94 -23
  20. package/registry/admin-apps/nextjs/variants/workspaces/package.json +11 -2
  21. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/account/page.tsx +179 -41
  22. package/registry/admin-apps/react/shared/.husky/commit-msg +1 -0
  23. package/registry/admin-apps/react/shared/.husky/pre-commit +1 -0
  24. package/registry/admin-apps/react/shared/.lintstagedrc.json +4 -0
  25. package/registry/admin-apps/react/shared/.prettierrc.json +3 -0
  26. package/registry/admin-apps/react/shared/commitlint.config.js +3 -0
  27. package/registry/admin-apps/react/shared/eslint.config.js +38 -0
  28. package/registry/admin-apps/react/shared/gitignore +17 -0
  29. package/registry/admin-apps/react/variants/base/package.json +15 -2
  30. package/registry/admin-apps/react/variants/base/src/pages/CountryDetailPage.tsx +76 -19
  31. package/registry/admin-apps/react/variants/base/src/pages/CustomerDetailPage.tsx +110 -26
  32. package/registry/admin-apps/react/variants/base/src/pages/EditCountryPage.tsx +115 -29
  33. package/registry/admin-apps/react/variants/base/src/pages/EditCustomerPage.tsx +123 -31
  34. package/registry/admin-apps/react/variants/base/src/pages/EditLanguagePage.tsx +100 -24
  35. package/registry/admin-apps/react/variants/base/src/pages/EditRolePage.tsx +99 -26
  36. package/registry/admin-apps/react/variants/base/src/pages/EditUserPage.tsx +126 -31
  37. package/registry/admin-apps/react/variants/base/src/pages/LanguageDetailPage.tsx +75 -19
  38. package/registry/admin-apps/react/variants/base/src/pages/PermissionDetailPage.tsx +58 -13
  39. package/registry/admin-apps/react/variants/base/src/pages/PermissionsPage.tsx +132 -29
  40. package/registry/admin-apps/react/variants/base/src/pages/RoleDetailPage.tsx +70 -17
  41. package/registry/admin-apps/react/variants/base/src/pages/RolesPage.tsx +277 -59
  42. package/registry/admin-apps/react/variants/workspaces/package.json +15 -2
  43. package/registry/combos/express-drizzle/eslint.config.js +18 -0
  44. package/registry/combos/express-drizzle/package.json +5 -1
  45. package/registry/combos/express-drizzle/shared/src/request-context.ts +3 -0
  46. package/registry/combos/express-prisma/eslint.config.js +18 -0
  47. package/registry/combos/express-prisma/package.json +5 -1
  48. package/registry/combos/express-prisma/shared/src/request-context.ts +3 -0
  49. package/registry/combos/nestjs-drizzle/eslint.config.js +18 -0
  50. package/registry/combos/nestjs-drizzle/package.json +5 -1
  51. package/registry/combos/nestjs-drizzle/shared/src/auth.controller.ts +138 -33
  52. package/registry/combos/nestjs-drizzle/shared/src/route-tiers.ts +71 -18
  53. package/registry/combos/nestjs-prisma/eslint.config.js +18 -0
  54. package/registry/combos/nestjs-prisma/package.json +5 -1
  55. package/registry/combos/nestjs-prisma/shared/src/auth.controller.ts +148 -35
  56. package/registry/combos/nestjs-prisma/shared/src/route-tiers.ts +72 -18
  57. package/registry/mobile-apps/bare-rn/shared/.husky/commit-msg +1 -0
  58. package/registry/mobile-apps/bare-rn/shared/.husky/pre-commit +1 -0
  59. package/registry/mobile-apps/bare-rn/shared/.lintstagedrc.json +4 -0
  60. package/registry/mobile-apps/bare-rn/shared/.prettierrc.json +5 -0
  61. package/registry/mobile-apps/bare-rn/shared/commitlint.config.js +3 -0
  62. package/registry/mobile-apps/bare-rn/shared/eslint.config.js +19 -0
  63. package/registry/mobile-apps/bare-rn/shared/gitignore +78 -0
  64. package/registry/mobile-apps/bare-rn/variants/base/package.json +17 -12
  65. package/registry/mobile-apps/bare-rn/variants/workspaces/package.json +17 -12
  66. package/registry/mobile-apps/expo/shared/.husky/commit-msg +1 -0
  67. package/registry/mobile-apps/expo/shared/.husky/pre-commit +1 -0
  68. package/registry/mobile-apps/expo/shared/.lintstagedrc.json +4 -0
  69. package/registry/mobile-apps/expo/shared/.prettierrc.json +5 -0
  70. package/registry/mobile-apps/expo/shared/commitlint.config.js +3 -0
  71. package/registry/mobile-apps/expo/shared/eslint.config.js +9 -0
  72. package/registry/mobile-apps/expo/shared/gitignore +44 -0
  73. package/registry/mobile-apps/expo/shared/src/screens/SessionsScreen.tsx +6 -17
  74. package/registry/mobile-apps/expo/shared/src/store/sessionsStore.ts +35 -0
  75. package/registry/mobile-apps/expo/variants/base/package.json +12 -3
  76. package/registry/mobile-apps/expo/variants/workspaces/package.json +12 -3
  77. package/registry/mobile-apps/expo/variants/workspaces/src/screens/WorkspaceSelectScreen.tsx +1 -1
  78. package/registry.json +13 -36
  79. package/registry/mobile-apps/bare-rn/shared/.eslintrc.js +0 -4
  80. package/registry/mobile-apps/bare-rn/shared/.prettierrc.js +0 -5
@@ -3,17 +3,33 @@
3
3
  import { observer } from "mobx-react-lite";
4
4
  import { QRCodeSVG } from "qrcode.react";
5
5
  import { useEffect, useState } from "react";
6
- import { AuthApiError, type SessionSummary } from "@simple-auth-kit/auth-client";
6
+ import {
7
+ AuthApiError,
8
+ type SessionSummary,
9
+ } from "@simple-auth-kit/auth-client";
7
10
  import { toast } from "sonner";
8
11
  import { ChangePasswordDialog } from "@/components/change-password-dialog";
9
12
  import { PhotoUpload } from "@/components/photo-upload";
10
13
  import { Alert } from "@/components/ui/alert";
11
14
  import { Badge } from "@/components/ui/badge";
12
15
  import { Button } from "@/components/ui/button";
13
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
16
+ import {
17
+ Card,
18
+ CardContent,
19
+ CardDescription,
20
+ CardHeader,
21
+ CardTitle,
22
+ } from "@/components/ui/card";
14
23
  import { Input } from "@/components/ui/input";
15
24
  import { Label } from "@/components/ui/label";
16
- import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
25
+ import {
26
+ Table,
27
+ TableBody,
28
+ TableCell,
29
+ TableHead,
30
+ TableHeader,
31
+ TableRow,
32
+ } from "@/components/ui/table";
17
33
  import { authClient } from "@/lib/auth-client";
18
34
  import { useAuthStore, useWorkspaceStore } from "@/lib/stores/store-context";
19
35
 
@@ -32,9 +48,21 @@ function initialsOf(label: string): string {
32
48
  );
33
49
  }
34
50
 
35
- type ProfileForm = { firstName: string; lastName: string; displayName: string; phone: string; username: string };
51
+ type ProfileForm = {
52
+ firstName: string;
53
+ lastName: string;
54
+ displayName: string;
55
+ phone: string;
56
+ username: string;
57
+ };
36
58
 
37
- function toForm(user: { firstName: string | null; lastName: string | null; displayName: string | null; phone: string | null; username: string | null }): ProfileForm {
59
+ function toForm(user: {
60
+ firstName: string | null;
61
+ lastName: string | null;
62
+ displayName: string | null;
63
+ phone: string | null;
64
+ username: string | null;
65
+ }): ProfileForm {
38
66
  return {
39
67
  firstName: user.firstName ?? "",
40
68
  lastName: user.lastName ?? "",
@@ -50,13 +78,18 @@ export default observer(function AccountPage() {
50
78
  const user = store.currentUser;
51
79
  const workspaceName = workspaces.activeWorkspace?.name ?? null;
52
80
 
53
- const [form, setForm] = useState<ProfileForm | null>(user ? toForm(user) : null);
81
+ const [form, setForm] = useState<ProfileForm | null>(
82
+ user ? toForm(user) : null,
83
+ );
54
84
  const [profileSaving, setProfileSaving] = useState(false);
55
85
  const [changePasswordOpen, setChangePasswordOpen] = useState(false);
56
86
 
57
87
  const [sessions, setSessions] = useState<SessionSummary[]>([]);
58
88
 
59
- const [enrollment, setEnrollment] = useState<{ secret: string; provisioningUri: string } | null>(null);
89
+ const [enrollment, setEnrollment] = useState<{
90
+ secret: string;
91
+ provisioningUri: string;
92
+ } | null>(null);
60
93
  const [enrollCode, setEnrollCode] = useState("");
61
94
  const [backupCodes, setBackupCodes] = useState<string[] | null>(null);
62
95
  const [disableCode, setDisableCode] = useState("");
@@ -67,7 +100,9 @@ export default observer(function AccountPage() {
67
100
  store
68
101
  .listSessions()
69
102
  .then(setSessions)
70
- .catch((err) => toast.error(apiErrorMessage(err, "Couldn't load active sessions.")));
103
+ .catch((err) =>
104
+ toast.error(apiErrorMessage(err, "Couldn't load active sessions.")),
105
+ );
71
106
  }, [store]);
72
107
 
73
108
  async function handleEnroll() {
@@ -76,7 +111,9 @@ export default observer(function AccountPage() {
76
111
  const result = await store.enrollTwoFactor();
77
112
  setEnrollment(result);
78
113
  } catch (err) {
79
- toast.error(apiErrorMessage(err, "Couldn't start 2FA enrollment. Try again."));
114
+ toast.error(
115
+ apiErrorMessage(err, "Couldn't start 2FA enrollment. Try again."),
116
+ );
80
117
  } finally {
81
118
  setTwoFactorBusy(false);
82
119
  }
@@ -91,7 +128,9 @@ export default observer(function AccountPage() {
91
128
  setEnrollCode("");
92
129
  toast.success("Two-factor authentication enabled.");
93
130
  } catch (err) {
94
- toast.error(apiErrorMessage(err, "Couldn't confirm this code. Try again."));
131
+ toast.error(
132
+ apiErrorMessage(err, "Couldn't confirm this code. Try again."),
133
+ );
95
134
  } finally {
96
135
  setTwoFactorBusy(false);
97
136
  }
@@ -115,9 +154,13 @@ export default observer(function AccountPage() {
115
154
  setLogoutAllBusy(true);
116
155
  try {
117
156
  await store.logoutAll();
118
- toast.success("Signed out of all sessions. You'll need to sign in again.");
157
+ toast.success(
158
+ "Signed out of all sessions. You'll need to sign in again.",
159
+ );
119
160
  } catch (err) {
120
- toast.error(apiErrorMessage(err, "Couldn't sign out of all sessions. Try again."));
161
+ toast.error(
162
+ apiErrorMessage(err, "Couldn't sign out of all sessions. Try again."),
163
+ );
121
164
  } finally {
122
165
  setLogoutAllBusy(false);
123
166
  }
@@ -130,7 +173,9 @@ export default observer(function AccountPage() {
130
173
  await store.refreshCurrentUser();
131
174
  toast.success(photo ? "Photo updated." : "Photo removed.");
132
175
  } catch (err) {
133
- toast.error(apiErrorMessage(err, "Couldn't update the photo. Try again."));
176
+ toast.error(
177
+ apiErrorMessage(err, "Couldn't update the photo. Try again."),
178
+ );
134
179
  }
135
180
  }
136
181
 
@@ -150,7 +195,9 @@ export default observer(function AccountPage() {
150
195
  await store.refreshCurrentUser();
151
196
  toast.success("Profile saved.");
152
197
  } catch (err) {
153
- toast.error(apiErrorMessage(err, "Couldn't save this profile. Try again."));
198
+ toast.error(
199
+ apiErrorMessage(err, "Couldn't save this profile. Try again."),
200
+ );
154
201
  } finally {
155
202
  setProfileSaving(false);
156
203
  }
@@ -163,37 +210,74 @@ export default observer(function AccountPage() {
163
210
  <Card>
164
211
  <CardHeader>
165
212
  <CardTitle>Profile</CardTitle>
166
- <CardDescription>Your own info — no admin permission needed to change any of this.</CardDescription>
213
+ <CardDescription>
214
+ Your own info — no admin permission needed to change any of this.
215
+ </CardDescription>
167
216
  </CardHeader>
168
217
  <CardContent className="flex flex-col gap-4">
169
- <PhotoUpload photo={user.photo} fallback={initialsOf(user.displayName || user.email)} onChange={handlePhotoChange} />
218
+ <PhotoUpload
219
+ photo={user.photo}
220
+ fallback={initialsOf(user.displayName || user.email)}
221
+ onChange={handlePhotoChange}
222
+ />
170
223
 
171
- <form onSubmit={handleProfileSave} className="grid grid-cols-1 gap-4 sm:grid-cols-2">
224
+ <form
225
+ onSubmit={handleProfileSave}
226
+ className="grid grid-cols-1 gap-4 sm:grid-cols-2"
227
+ >
172
228
  <div className="flex flex-col gap-1.5">
173
229
  <Label htmlFor="firstName">First name</Label>
174
- <Input id="firstName" value={form.firstName} onChange={(e) => setForm({ ...form, firstName: e.target.value })} />
230
+ <Input
231
+ id="firstName"
232
+ value={form.firstName}
233
+ onChange={(e) =>
234
+ setForm({ ...form, firstName: e.target.value })
235
+ }
236
+ />
175
237
  </div>
176
238
  <div className="flex flex-col gap-1.5">
177
239
  <Label htmlFor="lastName">Last name</Label>
178
- <Input id="lastName" value={form.lastName} onChange={(e) => setForm({ ...form, lastName: e.target.value })} />
240
+ <Input
241
+ id="lastName"
242
+ value={form.lastName}
243
+ onChange={(e) => setForm({ ...form, lastName: e.target.value })}
244
+ />
179
245
  </div>
180
246
  <div className="flex flex-col gap-1.5">
181
247
  <Label htmlFor="displayName">Display name</Label>
182
- <Input id="displayName" value={form.displayName} onChange={(e) => setForm({ ...form, displayName: e.target.value })} />
248
+ <Input
249
+ id="displayName"
250
+ value={form.displayName}
251
+ onChange={(e) =>
252
+ setForm({ ...form, displayName: e.target.value })
253
+ }
254
+ />
183
255
  </div>
184
256
  <div className="flex flex-col gap-1.5">
185
257
  <Label htmlFor="username">Username</Label>
186
- <Input id="username" value={form.username} onChange={(e) => setForm({ ...form, username: e.target.value })} />
258
+ <Input
259
+ id="username"
260
+ value={form.username}
261
+ onChange={(e) => setForm({ ...form, username: e.target.value })}
262
+ />
187
263
  </div>
188
264
  <div className="flex flex-col gap-1.5">
189
265
  <Label htmlFor="phone">Phone</Label>
190
- <Input id="phone" value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} />
266
+ <Input
267
+ id="phone"
268
+ value={form.phone}
269
+ onChange={(e) => setForm({ ...form, phone: e.target.value })}
270
+ />
191
271
  </div>
192
272
  <div className="flex items-end gap-2">
193
273
  <Button type="submit" disabled={profileSaving}>
194
274
  {profileSaving ? "Saving…" : "Save changes"}
195
275
  </Button>
196
- <Button type="button" variant="outline" onClick={() => setChangePasswordOpen(true)}>
276
+ <Button
277
+ type="button"
278
+ variant="outline"
279
+ onClick={() => setChangePasswordOpen(true)}
280
+ >
197
281
  Change password
198
282
  </Button>
199
283
  </div>
@@ -243,7 +327,9 @@ export default observer(function AccountPage() {
243
327
  </div>
244
328
  <div>
245
329
  <div className="text-xs text-muted-foreground">Two-factor</div>
246
- <Badge variant={user.twoFactorEnabled ? "success" : "outline"}>{user.twoFactorEnabled ? "Enabled" : "Disabled"}</Badge>
330
+ <Badge variant={user.twoFactorEnabled ? "success" : "outline"}>
331
+ {user.twoFactorEnabled ? "Enabled" : "Disabled"}
332
+ </Badge>
247
333
  </div>
248
334
  </CardContent>
249
335
  </Card>
@@ -251,7 +337,10 @@ export default observer(function AccountPage() {
251
337
  <Card>
252
338
  <CardHeader>
253
339
  <CardTitle>Active sessions</CardTitle>
254
- <CardDescription>Only bulk sign-out is available — the backend doesn't yet expose per-session revocation.</CardDescription>
340
+ <CardDescription>
341
+ Only bulk sign-out is available — the backend doesn&apos;t yet
342
+ expose per-session revocation.
343
+ </CardDescription>
255
344
  </CardHeader>
256
345
  <CardContent className="flex flex-col gap-4">
257
346
  <Table>
@@ -274,21 +363,35 @@ export default observer(function AccountPage() {
274
363
  </Badge>
275
364
  )}
276
365
  </TableCell>
277
- <TableCell className="text-xs text-muted-foreground">{new Date(session.createdAt).toLocaleString()}</TableCell>
278
- <TableCell className="max-w-xs truncate text-xs text-muted-foreground">{session.userAgent ?? "—"}</TableCell>
279
- <TableCell className="text-xs text-muted-foreground">{session.ip ?? "—"}</TableCell>
366
+ <TableCell className="text-xs text-muted-foreground">
367
+ {new Date(session.createdAt).toLocaleString()}
368
+ </TableCell>
369
+ <TableCell className="max-w-xs truncate text-xs text-muted-foreground">
370
+ {session.userAgent ?? "—"}
371
+ </TableCell>
372
+ <TableCell className="text-xs text-muted-foreground">
373
+ {session.ip ?? "—"}
374
+ </TableCell>
280
375
  </TableRow>
281
376
  ))}
282
377
  {sessions.length === 0 && (
283
378
  <TableRow>
284
- <TableCell colSpan={4} className="text-center text-sm text-muted-foreground">
379
+ <TableCell
380
+ colSpan={4}
381
+ className="text-center text-sm text-muted-foreground"
382
+ >
285
383
  No active sessions.
286
384
  </TableCell>
287
385
  </TableRow>
288
386
  )}
289
387
  </TableBody>
290
388
  </Table>
291
- <Button variant="destructive" className="w-fit" disabled={logoutAllBusy} onClick={handleLogoutAll}>
389
+ <Button
390
+ variant="destructive"
391
+ className="w-fit"
392
+ disabled={logoutAllBusy}
393
+ onClick={handleLogoutAll}
394
+ >
292
395
  {logoutAllBusy ? "Signing out…" : "Sign out of all sessions"}
293
396
  </Button>
294
397
  </CardContent>
@@ -301,7 +404,9 @@ export default observer(function AccountPage() {
301
404
  <CardContent className="flex flex-col gap-4">
302
405
  {backupCodes && (
303
406
  <Alert variant="success">
304
- <p className="mb-2 font-medium">Save these backup codes — they won&apos;t be shown again:</p>
407
+ <p className="mb-2 font-medium">
408
+ Save these backup codes — they won&apos;t be shown again:
409
+ </p>
305
410
  <ul className="grid grid-cols-2 gap-1 font-mono text-xs sm:grid-cols-5">
306
411
  {backupCodes.map((code) => (
307
412
  <li key={code}>{code}</li>
@@ -312,30 +417,54 @@ export default observer(function AccountPage() {
312
417
 
313
418
  {user.twoFactorEnabled ? (
314
419
  <div className="flex flex-col gap-3">
315
- <p className="text-sm text-muted-foreground">Two-factor authentication is enabled on your account.</p>
420
+ <p className="text-sm text-muted-foreground">
421
+ Two-factor authentication is enabled on your account.
422
+ </p>
316
423
  <div className="flex items-end gap-2">
317
424
  <div className="flex flex-col gap-1.5">
318
425
  <Label htmlFor="disableCode">Enter a code to disable</Label>
319
- <Input id="disableCode" value={disableCode} onChange={(e) => setDisableCode(e.target.value)} placeholder="123456" />
426
+ <Input
427
+ id="disableCode"
428
+ value={disableCode}
429
+ onChange={(e) => setDisableCode(e.target.value)}
430
+ placeholder="123456"
431
+ />
320
432
  </div>
321
- <Button variant="destructive" disabled={twoFactorBusy || !disableCode} onClick={handleDisable}>
433
+ <Button
434
+ variant="destructive"
435
+ disabled={twoFactorBusy || !disableCode}
436
+ onClick={handleDisable}
437
+ >
322
438
  {twoFactorBusy ? "Disabling…" : "Disable 2FA"}
323
439
  </Button>
324
440
  </div>
325
441
  </div>
326
442
  ) : enrollment ? (
327
443
  <div className="flex flex-col gap-3">
328
- <p className="text-sm text-muted-foreground">Scan this QR code with your authenticator app, then enter the 6-digit code it shows.</p>
444
+ <p className="text-sm text-muted-foreground">
445
+ Scan this QR code with your authenticator app, then enter the
446
+ 6-digit code it shows.
447
+ </p>
329
448
  <div className="w-fit rounded-md border border-border bg-white p-3">
330
449
  <QRCodeSVG value={enrollment.provisioningUri} size={176} />
331
450
  </div>
332
- <p className="font-mono text-xs text-muted-foreground">Secret: {enrollment.secret}</p>
451
+ <p className="font-mono text-xs text-muted-foreground">
452
+ Secret: {enrollment.secret}
453
+ </p>
333
454
  <div className="flex items-end gap-2">
334
455
  <div className="flex flex-col gap-1.5">
335
456
  <Label htmlFor="enrollCode">Confirmation code</Label>
336
- <Input id="enrollCode" value={enrollCode} onChange={(e) => setEnrollCode(e.target.value)} placeholder="123456" />
457
+ <Input
458
+ id="enrollCode"
459
+ value={enrollCode}
460
+ onChange={(e) => setEnrollCode(e.target.value)}
461
+ placeholder="123456"
462
+ />
337
463
  </div>
338
- <Button disabled={twoFactorBusy || !enrollCode} onClick={handleConfirm}>
464
+ <Button
465
+ disabled={twoFactorBusy || !enrollCode}
466
+ onClick={handleConfirm}
467
+ >
339
468
  {twoFactorBusy ? "Confirming…" : "Confirm"}
340
469
  </Button>
341
470
  <Button variant="ghost" onClick={() => setEnrollment(null)}>
@@ -345,8 +474,14 @@ export default observer(function AccountPage() {
345
474
  </div>
346
475
  ) : (
347
476
  <div className="flex flex-col gap-3">
348
- <p className="text-sm text-muted-foreground">Two-factor authentication is not enabled on your account.</p>
349
- <Button className="w-fit" disabled={twoFactorBusy} onClick={handleEnroll}>
477
+ <p className="text-sm text-muted-foreground">
478
+ Two-factor authentication is not enabled on your account.
479
+ </p>
480
+ <Button
481
+ className="w-fit"
482
+ disabled={twoFactorBusy}
483
+ onClick={handleEnroll}
484
+ >
350
485
  {twoFactorBusy ? "Starting…" : "Enable 2FA"}
351
486
  </Button>
352
487
  </div>
@@ -354,7 +489,10 @@ export default observer(function AccountPage() {
354
489
  </CardContent>
355
490
  </Card>
356
491
 
357
- <ChangePasswordDialog open={changePasswordOpen} onOpenChange={setChangePasswordOpen} />
492
+ <ChangePasswordDialog
493
+ open={changePasswordOpen}
494
+ onOpenChange={setChangePasswordOpen}
495
+ />
358
496
  </div>
359
497
  );
360
498
  });
@@ -0,0 +1 @@
1
+ npx --no -- commitlint --edit "$1"
@@ -0,0 +1 @@
1
+ npx lint-staged
@@ -0,0 +1,4 @@
1
+ {
2
+ "*.{ts,tsx,js,jsx}": "eslint --fix",
3
+ "*.{ts,tsx,js,jsx,json,md,css,scss}": "prettier --write"
4
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "trailingComma": "all"
3
+ }
@@ -0,0 +1,3 @@
1
+ export default {
2
+ extends: ["@commitlint/config-conventional"],
3
+ };
@@ -0,0 +1,38 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+ import tseslint from "typescript-eslint";
6
+
7
+ export default tseslint.config(
8
+ {
9
+ // src/components/ui/** is shadcn-generated (components.json), not hand-authored — same
10
+ // "vendored, don't lint" treatment this repo already gives it for Biome (the surviving
11
+ // biome-ignore-all comment on multi-selector.tsx).
12
+ ignores: ["dist/**", "node_modules/**", "src/components/ui/**"],
13
+ },
14
+ {
15
+ files: ["**/*.{ts,tsx}"],
16
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
17
+ languageOptions: {
18
+ ecmaVersion: 2022,
19
+ globals: globals.browser,
20
+ },
21
+ plugins: {
22
+ "react-hooks": reactHooks,
23
+ "react-refresh": reactRefresh,
24
+ },
25
+ rules: {
26
+ ...reactHooks.configs.recommended.rules,
27
+ "react-refresh/only-export-components": [
28
+ "warn",
29
+ { allowConstantExport: true },
30
+ ],
31
+ // TanStack Table's useReactTable() is on React Compiler's own documented list of
32
+ // incompatible APIs (it returns functions that can't be safely memoized) — the compiler
33
+ // already handles this correctly by skipping memoization for it; there's no code change
34
+ // that "fixes" a structural library incompatibility.
35
+ "react-hooks/incompatible-library": "off",
36
+ },
37
+ },
38
+ );
@@ -0,0 +1,17 @@
1
+ # dependencies
2
+ node_modules/
3
+
4
+ # build output
5
+ dist/
6
+
7
+ # local env files
8
+ .env*.local
9
+
10
+ # typescript
11
+ *.tsbuildinfo
12
+
13
+ # macOS
14
+ .DS_Store
15
+
16
+ # husky-generated shim dir (recreated by the "prepare" script on install)
17
+ .husky/_
@@ -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
  }