@alepha/ui 0.15.4 → 0.16.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 (102) hide show
  1. package/dist/admin/AdminApiKeys-DsmGnHNh.js +3 -0
  2. package/dist/admin/AdminApiKeys-GMORg-1l.js +442 -0
  3. package/dist/admin/AdminApiKeys-GMORg-1l.js.map +1 -0
  4. package/dist/admin/AdminAudits-8SM96viT.js +3 -0
  5. package/dist/admin/{AdminAudits-Oh7iAfQa.js → AdminAudits-pkWrjq1Z.js} +2 -2
  6. package/dist/admin/{AdminAudits-Oh7iAfQa.js.map → AdminAudits-pkWrjq1Z.js.map} +1 -1
  7. package/dist/admin/AdminFiles-B56ocq4H.js +3 -0
  8. package/dist/admin/{AdminFiles-Cu8GHgQ3.js → AdminFiles-WeQbsCsl.js} +2 -2
  9. package/dist/admin/{AdminFiles-Cu8GHgQ3.js.map → AdminFiles-WeQbsCsl.js.map} +1 -1
  10. package/dist/admin/AdminJobs-B-q9iGO3.js +697 -0
  11. package/dist/admin/AdminJobs-B-q9iGO3.js.map +1 -0
  12. package/dist/admin/AdminJobs-CED1syCn.js +3 -0
  13. package/dist/admin/{AdminLayout-QJLIesuG.js → AdminLayout-D8yZ-8lG.js} +4 -2
  14. package/dist/admin/AdminLayout-D8yZ-8lG.js.map +1 -0
  15. package/dist/admin/AdminNotifications-B0B1rdc4.js +3 -0
  16. package/dist/admin/{AdminNotifications-CgYkBuG_.js → AdminNotifications-Ds5Un0NJ.js} +2 -2
  17. package/dist/admin/{AdminNotifications-CgYkBuG_.js.map → AdminNotifications-Ds5Un0NJ.js.map} +1 -1
  18. package/dist/admin/{AdminParameters-Cl-R0nXt.js → AdminParameters-BU3lATdJ.js} +1 -1
  19. package/dist/admin/{AdminParameters-hjNG_KXb.js → AdminParameters-CfDUpc78.js} +4 -4
  20. package/dist/admin/{AdminParameters-hjNG_KXb.js.map → AdminParameters-CfDUpc78.js.map} +1 -1
  21. package/dist/admin/AdminSessions-BDGK2MS6.js +3 -0
  22. package/dist/admin/{AdminSessions-Bey9cuy1.js → AdminSessions-DzIOxM3b.js} +2 -2
  23. package/dist/admin/{AdminSessions-Bey9cuy1.js.map → AdminSessions-DzIOxM3b.js.map} +1 -1
  24. package/dist/admin/{AdminUserAudits-C7AN9jx7.js → AdminUserAudits-CiUPN2BC.js} +2 -2
  25. package/dist/admin/{AdminUserAudits-C7AN9jx7.js.map → AdminUserAudits-CiUPN2BC.js.map} +1 -1
  26. package/dist/admin/{AdminUserAudits-Cp_ERd2g.js → AdminUserAudits-Cj79gENT.js} +1 -1
  27. package/dist/admin/{AdminUserCreate-BVIm4JdN.js → AdminUserCreate-BwQKr4xE.js} +2 -2
  28. package/dist/admin/{AdminUserCreate-BVIm4JdN.js.map → AdminUserCreate-BwQKr4xE.js.map} +1 -1
  29. package/dist/admin/{AdminUserCreate-C1aInRDk.js → AdminUserCreate-Cq-mUmBs.js} +1 -1
  30. package/dist/admin/{AdminUserDetails-Dcn3OwMC.js → AdminUserDetails-DRjVAPFd.js} +1 -1
  31. package/dist/admin/{AdminUserDetails-yM4x8JE6.js → AdminUserDetails-uqtC5aJ1.js} +2 -2
  32. package/dist/admin/{AdminUserDetails-yM4x8JE6.js.map → AdminUserDetails-uqtC5aJ1.js.map} +1 -1
  33. package/dist/admin/{AdminUserLayout-gb-nbggz.js → AdminUserLayout-CGzmHHby.js} +1 -1
  34. package/dist/admin/{AdminUserLayout-BnfBC1gD.js → AdminUserLayout-CiPay35T.js} +2 -2
  35. package/dist/admin/{AdminUserLayout-BnfBC1gD.js.map → AdminUserLayout-CiPay35T.js.map} +1 -1
  36. package/dist/admin/{AdminUserSessions-kmkXG-xf.js → AdminUserSessions-DAE8Nf1F.js} +2 -2
  37. package/dist/admin/{AdminUserSessions-kmkXG-xf.js.map → AdminUserSessions-DAE8Nf1F.js.map} +1 -1
  38. package/dist/admin/AdminUserSessions-DcdzuNZ9.js +3 -0
  39. package/dist/admin/AdminUserSettings-D7V6-ceX.js +3 -0
  40. package/dist/admin/{AdminUserSettings-DZ9iWhJW.js → AdminUserSettings-EbahaV2a.js} +2 -2
  41. package/dist/admin/{AdminUserSettings-DZ9iWhJW.js.map → AdminUserSettings-EbahaV2a.js.map} +1 -1
  42. package/dist/admin/AdminUsers-D9nyzGqQ.js +3 -0
  43. package/dist/admin/{AdminUsers-D6Y5K8Am.js → AdminUsers-Dcjh0KNW.js} +2 -2
  44. package/dist/admin/{AdminUsers-D6Y5K8Am.js.map → AdminUsers-Dcjh0KNW.js.map} +1 -1
  45. package/dist/admin/index.d.ts +33 -41
  46. package/dist/admin/index.d.ts.map +1 -1
  47. package/dist/admin/index.js +129 -179
  48. package/dist/admin/index.js.map +1 -1
  49. package/dist/auth/{AuthLayout-Dj5K4SIN.js → AuthLayout-mFOWbiSP.js} +2 -1
  50. package/dist/auth/AuthLayout-mFOWbiSP.js.map +1 -0
  51. package/dist/auth/index.d.ts +18 -8
  52. package/dist/auth/index.d.ts.map +1 -1
  53. package/dist/auth/index.js +22 -17
  54. package/dist/auth/index.js.map +1 -1
  55. package/dist/core/index.d.ts +16 -24
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +34 -43
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/demo/{DemoLogin-S-b15cmE.js → DemoLogin-CvCG2WVh.js} +3 -1
  60. package/dist/demo/{DemoLogin-S-b15cmE.js.map → DemoLogin-CvCG2WVh.js.map} +1 -1
  61. package/dist/demo/{DemoRegister-B29MdAaZ.js → DemoRegister-CmeHbOAs.js} +3 -1
  62. package/dist/demo/{DemoRegister-B29MdAaZ.js.map → DemoRegister-CmeHbOAs.js.map} +1 -1
  63. package/dist/demo/{DemoResetPassword-CPTy88iK.js → DemoResetPassword-CKO5iA_6.js} +3 -1
  64. package/dist/demo/{DemoResetPassword-CPTy88iK.js.map → DemoResetPassword-CKO5iA_6.js.map} +1 -1
  65. package/dist/demo/index.js +3 -3
  66. package/package.json +11 -11
  67. package/src/admin/AdminRouter.ts +111 -2
  68. package/src/admin/components/AdminLayout.tsx +2 -0
  69. package/src/admin/components/jobs/AdminJobs.tsx +733 -119
  70. package/src/admin/components/keys/AdminApiKeys.tsx +537 -0
  71. package/src/admin/components/parameters/AdminParameters.tsx +2 -3
  72. package/src/admin/index.ts +8 -17
  73. package/src/admin/primitives/$uiAdmin.ts +17 -0
  74. package/src/auth/AuthRouter.ts +14 -6
  75. package/src/auth/components/AuthLayout.tsx +1 -0
  76. package/src/auth/index.ts +5 -14
  77. package/src/auth/primitives/$uiAuth.ts +10 -0
  78. package/src/core/components/buttons/ActionButton.tsx +15 -2
  79. package/src/core/components/buttons/DarkModeButton.css +6 -0
  80. package/src/core/components/buttons/DarkModeButton.tsx +18 -71
  81. package/src/core/components/buttons/LanguageButton.tsx +2 -7
  82. package/src/core/components/buttons/ThemeButton.tsx +2 -6
  83. package/src/core/components/layout/AdminShell.tsx +17 -1
  84. package/src/core/components/layout/AlephaMantineProvider.tsx +2 -1
  85. package/src/core/components/layout/AppBar.tsx +5 -8
  86. package/src/core/components/layout/Sidebar.tsx +10 -0
  87. package/src/core/index.ts +3 -4
  88. package/src/core/styles.css +1 -0
  89. package/src/demo/components/auth/DemoLogin.tsx +2 -0
  90. package/src/demo/components/auth/DemoRegister.tsx +2 -0
  91. package/src/demo/components/auth/DemoResetPassword.tsx +2 -0
  92. package/dist/admin/AdminAudits-BU-p1g7A.js +0 -3
  93. package/dist/admin/AdminFiles-Bg9feLFH.js +0 -3
  94. package/dist/admin/AdminLayout-BfeFXiul.js +0 -3
  95. package/dist/admin/AdminLayout-QJLIesuG.js.map +0 -1
  96. package/dist/admin/AdminNotifications-DmfGPqHe.js +0 -3
  97. package/dist/admin/AdminSessions-Cn4_jB04.js +0 -3
  98. package/dist/admin/AdminUserSessions-rvA0ztxn.js +0 -3
  99. package/dist/admin/AdminUserSettings-Dg-wTRzN.js +0 -3
  100. package/dist/admin/AdminUsers-RCaxccEW.js +0 -3
  101. package/dist/auth/AuthLayout-Dj5K4SIN.js.map +0 -1
  102. package/src/admin/MainRouter.ts +0 -23
@@ -0,0 +1,537 @@
1
+ import {
2
+ ActionButton,
3
+ ClipboardButton,
4
+ DataTable,
5
+ Flex,
6
+ Text,
7
+ useDialog,
8
+ useToast,
9
+ } from "@alepha/ui";
10
+ import {
11
+ ActionIcon,
12
+ Badge,
13
+ Box,
14
+ Code,
15
+ Group,
16
+ Paper,
17
+ RingProgress,
18
+ SimpleGrid,
19
+ Stack,
20
+ ThemeIcon,
21
+ Tooltip,
22
+ } from "@mantine/core";
23
+ import {
24
+ IconCalendarOff,
25
+ IconCheck,
26
+ IconClock,
27
+ IconKey,
28
+ IconNetwork,
29
+ IconShieldCheck,
30
+ IconShieldOff,
31
+ IconTrash,
32
+ IconUser,
33
+ } from "@tabler/icons-react";
34
+ import { type Page, t } from "alepha";
35
+ import type { AdminApiKeyController } from "alepha/api/keys";
36
+ import { useClient } from "alepha/react";
37
+ import { useI18n } from "alepha/react/i18n";
38
+ import { useRouter } from "alepha/react/router";
39
+ import { useCallback, useState } from "react";
40
+ import type { AdminRouter } from "../../AdminRouter.ts";
41
+
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+ // Types
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+
46
+ interface ApiKeyResource {
47
+ id: string;
48
+ userId: string;
49
+ name: string;
50
+ description?: string;
51
+ tokenPrefix: string;
52
+ tokenSuffix: string;
53
+ roles: string[];
54
+ createdAt: string;
55
+ lastUsedAt?: string;
56
+ lastUsedIp?: string;
57
+ expiresAt?: string;
58
+ revokedAt?: string;
59
+ usageCount: number;
60
+ }
61
+
62
+ interface KeyStats {
63
+ total: number;
64
+ active: number;
65
+ revoked: number;
66
+ expired: number;
67
+ neverUsed: number;
68
+ }
69
+
70
+ // ─────────────────────────────────────────────────────────────────────────────
71
+ // Utilities
72
+ // ─────────────────────────────────────────────────────────────────────────────
73
+
74
+ const getKeyStatus = (
75
+ key: ApiKeyResource,
76
+ ): "active" | "revoked" | "expired" => {
77
+ if (key.revokedAt) return "revoked";
78
+ if (key.expiresAt && new Date(key.expiresAt) < new Date()) return "expired";
79
+ return "active";
80
+ };
81
+
82
+ const getStatusColor = (status: "active" | "revoked" | "expired") => {
83
+ switch (status) {
84
+ case "active":
85
+ return "teal";
86
+ case "revoked":
87
+ return "red";
88
+ case "expired":
89
+ return "orange";
90
+ }
91
+ };
92
+
93
+ const getStatusIcon = (status: "active" | "revoked" | "expired") => {
94
+ switch (status) {
95
+ case "active":
96
+ return <IconShieldCheck size={14} />;
97
+ case "revoked":
98
+ return <IconShieldOff size={14} />;
99
+ case "expired":
100
+ return <IconCalendarOff size={14} />;
101
+ }
102
+ };
103
+
104
+ const formatKeyPreview = (prefix: string, suffix: string) => {
105
+ return `${prefix}...${suffix}`;
106
+ };
107
+
108
+ // ─────────────────────────────────────────────────────────────────────────────
109
+ // Stats Cards
110
+ // ─────────────────────────────────────────────────────────────────────────────
111
+
112
+ interface StatsCardsProps {
113
+ stats: KeyStats;
114
+ loading: boolean;
115
+ }
116
+
117
+ const StatsCards = ({ stats, loading }: StatsCardsProps) => {
118
+ const activePercentage =
119
+ stats.total > 0 ? Math.round((stats.active / stats.total) * 100) : 0;
120
+
121
+ return (
122
+ <SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
123
+ <Paper p="md" radius="md" withBorder>
124
+ <Group justify="space-between">
125
+ <Box>
126
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
127
+ Total Keys
128
+ </Text>
129
+ <Text size="xl" fw={700} ff="monospace">
130
+ {stats.total}
131
+ </Text>
132
+ </Box>
133
+ <ThemeIcon size="lg" radius="md" variant="light" color="blue">
134
+ <IconKey size={20} />
135
+ </ThemeIcon>
136
+ </Group>
137
+ </Paper>
138
+
139
+ <Paper p="md" radius="md" withBorder>
140
+ <Group justify="space-between">
141
+ <Box>
142
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
143
+ Active
144
+ </Text>
145
+ <Text size="xl" fw={700} ff="monospace" c="teal">
146
+ {stats.active}
147
+ </Text>
148
+ </Box>
149
+ <RingProgress
150
+ size={48}
151
+ thickness={4}
152
+ roundCaps
153
+ sections={[{ value: activePercentage, color: "teal" }]}
154
+ />
155
+ </Group>
156
+ </Paper>
157
+
158
+ <Paper p="md" radius="md" withBorder>
159
+ <Group justify="space-between">
160
+ <Box>
161
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
162
+ Revoked
163
+ </Text>
164
+ <Text size="xl" fw={700} ff="monospace" c="red">
165
+ {stats.revoked}
166
+ </Text>
167
+ </Box>
168
+ <ThemeIcon
169
+ size="lg"
170
+ radius="md"
171
+ variant="light"
172
+ color={stats.revoked > 0 ? "red" : "gray"}
173
+ >
174
+ <IconShieldOff size={20} />
175
+ </ThemeIcon>
176
+ </Group>
177
+ </Paper>
178
+
179
+ <Paper p="md" radius="md" withBorder>
180
+ <Group justify="space-between">
181
+ <Box>
182
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
183
+ Never Used
184
+ </Text>
185
+ <Text size="xl" fw={700} ff="monospace" c="yellow">
186
+ {stats.neverUsed}
187
+ </Text>
188
+ </Box>
189
+ <ThemeIcon
190
+ size="lg"
191
+ radius="md"
192
+ variant="light"
193
+ color={stats.neverUsed > 0 ? "yellow" : "gray"}
194
+ >
195
+ <IconClock size={20} />
196
+ </ThemeIcon>
197
+ </Group>
198
+ </Paper>
199
+ </SimpleGrid>
200
+ );
201
+ };
202
+
203
+ // ─────────────────────────────────────────────────────────────────────────────
204
+ // Main Component
205
+ // ─────────────────────────────────────────────────────────────────────────────
206
+
207
+ const AdminApiKeys = () => {
208
+ const client = useClient<AdminApiKeyController>();
209
+ const router = useRouter<AdminRouter>();
210
+ const { l } = useI18n();
211
+ const toast = useToast();
212
+ const dialog = useDialog();
213
+
214
+ const [stats, setStats] = useState<KeyStats>({
215
+ total: 0,
216
+ active: 0,
217
+ revoked: 0,
218
+ expired: 0,
219
+ neverUsed: 0,
220
+ });
221
+ const [refreshKey, setRefreshKey] = useState(0);
222
+ const [loading, setLoading] = useState(true);
223
+
224
+ const filters = t.object({
225
+ userId: t.optional(t.uuid()),
226
+ includeRevoked: t.optional(t.boolean()),
227
+ });
228
+
229
+ const handleRevoke = useCallback(
230
+ async (key: ApiKeyResource) => {
231
+ const confirmed = await dialog.confirm({
232
+ title: "Revoke API Key",
233
+ message: `Are you sure you want to revoke "${key.name}"? This action cannot be undone and will immediately invalidate the key.`,
234
+ confirmLabel: "Revoke Key",
235
+ confirmColor: "red",
236
+ });
237
+
238
+ if (!confirmed) return;
239
+
240
+ try {
241
+ await client.revokeApiKey({ params: { id: key.id } });
242
+ toast.success(`API key "${key.name}" has been revoked`);
243
+ setRefreshKey((k) => k + 1);
244
+ } catch (error) {
245
+ toast.danger(`Failed to revoke API key`);
246
+ }
247
+ },
248
+ [client, dialog, toast],
249
+ );
250
+
251
+ // Calculate stats from loaded data
252
+ const updateStats = useCallback((keys: ApiKeyResource[]) => {
253
+ const now = new Date();
254
+ const newStats: KeyStats = {
255
+ total: keys.length,
256
+ active: 0,
257
+ revoked: 0,
258
+ expired: 0,
259
+ neverUsed: 0,
260
+ };
261
+
262
+ for (const key of keys) {
263
+ if (key.revokedAt) {
264
+ newStats.revoked++;
265
+ } else if (key.expiresAt && new Date(key.expiresAt) < now) {
266
+ newStats.expired++;
267
+ } else {
268
+ newStats.active++;
269
+ }
270
+
271
+ if (!key.lastUsedAt) {
272
+ newStats.neverUsed++;
273
+ }
274
+ }
275
+
276
+ setStats(newStats);
277
+ setLoading(false);
278
+ }, []);
279
+
280
+ return (
281
+ <Flex flex={1} direction="column" gap="md">
282
+ {/* Stats Header */}
283
+ <StatsCards stats={stats} loading={loading} />
284
+
285
+ {/* API Keys Table */}
286
+ <DataTable<ApiKeyResource, typeof filters>
287
+ key={refreshKey}
288
+ submitOnInit
289
+ defaultSize={15}
290
+ typeFormProps={{
291
+ skipSubmitButton: true,
292
+ columns: 2,
293
+ }}
294
+ tableProps={{
295
+ horizontalSpacing: "sm",
296
+ verticalSpacing: "sm",
297
+ highlightOnHover: true,
298
+ }}
299
+ onFilterChange={(key, _value, form) => {
300
+ if (key === "userId" || key === "includeRevoked") {
301
+ return form.submit();
302
+ }
303
+ }}
304
+ filters={filters}
305
+ tableTrProps={(item) => {
306
+ const status = getKeyStatus(item);
307
+ if (status === "revoked") {
308
+ return {
309
+ opacity: 0.6,
310
+ style: {
311
+ textDecoration: "line-through",
312
+ textDecorationColor: "var(--mantine-color-red-5)",
313
+ },
314
+ };
315
+ }
316
+ if (status === "expired") {
317
+ return { opacity: 0.7 };
318
+ }
319
+ return {};
320
+ }}
321
+ items={async (filters) => {
322
+ const response = await client.findApiKeys({
323
+ query: {
324
+ ...filters,
325
+ includeRevoked: filters.includeRevoked ?? true,
326
+ },
327
+ });
328
+
329
+ // Update stats with all keys (need to fetch all for accurate stats)
330
+ const allKeys = await client.findApiKeys({
331
+ query: { includeRevoked: true, size: 100 },
332
+ });
333
+ updateStats(allKeys.content as ApiKeyResource[]);
334
+
335
+ return response as Page<ApiKeyResource>;
336
+ }}
337
+ columns={{
338
+ name: {
339
+ label: "Name",
340
+ value: (item) => (
341
+ <Stack gap={2}>
342
+ <Group gap="xs">
343
+ <ThemeIcon
344
+ size="xs"
345
+ radius="sm"
346
+ variant="light"
347
+ color={getStatusColor(getKeyStatus(item))}
348
+ >
349
+ <IconKey size={10} />
350
+ </ThemeIcon>
351
+ <Text size="sm" fw={600}>
352
+ {item.name}
353
+ </Text>
354
+ </Group>
355
+ {item.description && (
356
+ <Text size="xs" c="dimmed" lineClamp={1}>
357
+ {item.description}
358
+ </Text>
359
+ )}
360
+ </Stack>
361
+ ),
362
+ },
363
+ token: {
364
+ label: "Key",
365
+ fit: true,
366
+ value: (item) => (
367
+ <Group gap={4}>
368
+ <Code
369
+ ff="monospace"
370
+ style={{
371
+ fontSize: 11,
372
+ letterSpacing: "0.5px",
373
+ }}
374
+ >
375
+ {formatKeyPreview(item.tokenPrefix, item.tokenSuffix)}
376
+ </Code>
377
+ <ClipboardButton
378
+ size="xs"
379
+ variant="subtle"
380
+ value={formatKeyPreview(item.tokenPrefix, item.tokenSuffix)}
381
+ />
382
+ </Group>
383
+ ),
384
+ },
385
+ status: {
386
+ label: "Status",
387
+ fit: true,
388
+ value: (item) => {
389
+ const status = getKeyStatus(item);
390
+ return (
391
+ <Badge
392
+ size="sm"
393
+ variant="light"
394
+ color={getStatusColor(status)}
395
+ leftSection={getStatusIcon(status)}
396
+ >
397
+ {status.toUpperCase()}
398
+ </Badge>
399
+ );
400
+ },
401
+ },
402
+ roles: {
403
+ label: "Roles",
404
+ value: (item) => (
405
+ <Group gap={4} wrap="wrap">
406
+ {item.roles.length > 0 ? (
407
+ item.roles.slice(0, 3).map((role) => (
408
+ <Badge key={role} size="xs" variant="outline" color="gray">
409
+ {role}
410
+ </Badge>
411
+ ))
412
+ ) : (
413
+ <Text size="xs" c="dimmed">
414
+ No roles
415
+ </Text>
416
+ )}
417
+ {item.roles.length > 3 && (
418
+ <Tooltip label={item.roles.slice(3).join(", ")}>
419
+ <Badge size="xs" variant="light" color="gray">
420
+ +{item.roles.length - 3}
421
+ </Badge>
422
+ </Tooltip>
423
+ )}
424
+ </Group>
425
+ ),
426
+ },
427
+ usage: {
428
+ label: "Usage",
429
+ fit: true,
430
+ value: (item) => (
431
+ <Stack gap={2}>
432
+ <Text size="xs" ff="monospace" fw={500}>
433
+ {item.usageCount.toLocaleString()} calls
434
+ </Text>
435
+ {item.lastUsedAt ? (
436
+ <Group gap={4}>
437
+ <Text size="xs" c="dimmed">
438
+ {l(item.lastUsedAt, { date: "fromNow" })}
439
+ </Text>
440
+ {item.lastUsedIp && (
441
+ <Tooltip label={`Last IP: ${item.lastUsedIp}`}>
442
+ <IconNetwork
443
+ size={12}
444
+ color="var(--mantine-color-dimmed)"
445
+ />
446
+ </Tooltip>
447
+ )}
448
+ </Group>
449
+ ) : (
450
+ <Text size="xs" c="yellow">
451
+ Never used
452
+ </Text>
453
+ )}
454
+ </Stack>
455
+ ),
456
+ },
457
+ userId: {
458
+ label: "Owner",
459
+ fit: true,
460
+ value: (item) => (
461
+ <ActionButton
462
+ variant="subtle"
463
+ size="xs"
464
+ href={router.path("adminUserDetails", {
465
+ params: { userId: item.userId },
466
+ })}
467
+ leftSection={<IconUser size={12} />}
468
+ >
469
+ <Text size="xs" ff="monospace">
470
+ {item.userId.slice(0, 8)}
471
+ </Text>
472
+ </ActionButton>
473
+ ),
474
+ },
475
+ createdAt: {
476
+ label: "Created",
477
+ fit: true,
478
+ value: (item) => (
479
+ <Text size="xs" c="dimmed">
480
+ {l(item.createdAt, { date: "fromNow" })}
481
+ </Text>
482
+ ),
483
+ },
484
+ expiresAt: {
485
+ label: "Expires",
486
+ fit: true,
487
+ value: (item) => {
488
+ if (!item.expiresAt) {
489
+ return (
490
+ <Text size="xs" c="dimmed">
491
+ Never
492
+ </Text>
493
+ );
494
+ }
495
+
496
+ const isExpired = new Date(item.expiresAt) < new Date();
497
+ return (
498
+ <Text size="xs" c={isExpired ? "orange" : "dimmed"}>
499
+ {l(item.expiresAt, { date: "fromNow" })}
500
+ </Text>
501
+ );
502
+ },
503
+ },
504
+ actions: {
505
+ label: "",
506
+ fit: true,
507
+ value: (item) => {
508
+ const status = getKeyStatus(item);
509
+ if (status === "revoked") {
510
+ return (
511
+ <Tooltip label="Already revoked">
512
+ <IconCheck size={14} color="var(--mantine-color-dimmed)" />
513
+ </Tooltip>
514
+ );
515
+ }
516
+
517
+ return (
518
+ <Tooltip label="Revoke key">
519
+ <ActionIcon
520
+ size="sm"
521
+ variant="subtle"
522
+ color="red"
523
+ onClick={() => handleRevoke(item)}
524
+ >
525
+ <IconTrash size={14} />
526
+ </ActionIcon>
527
+ </Tooltip>
528
+ );
529
+ },
530
+ },
531
+ }}
532
+ />
533
+ </Flex>
534
+ );
535
+ };
536
+
537
+ export default AdminApiKeys;
@@ -41,12 +41,11 @@ const AdminParameters = () => {
41
41
  setLoadingHistory(true);
42
42
 
43
43
  try {
44
- const [current, historyData] = await Promise.all([
44
+ const [current] = await Promise.all([
45
45
  client.getCurrent({ params: { name } }),
46
- client.getHistory({ params: { name } }),
47
46
  ]);
48
47
  setConfigValue(current);
49
- setHistory(historyData.versions);
48
+ setHistory([]);
50
49
  } finally {
51
50
  setLoadingConfig(false);
52
51
  setLoadingHistory(false);
@@ -1,20 +1,19 @@
1
1
  import { AlephaUI } from "@alepha/ui";
2
2
  import { AlephaUIAuth } from "@alepha/ui/auth";
3
- import { $context, $module } from "alepha";
3
+ import { $module } from "alepha";
4
4
  import { AdminRouter } from "./AdminRouter.ts";
5
- import { MainRouter } from "./MainRouter.ts";
6
5
 
7
6
  // ---------------------------------------------------------------------------------------------------------------------
8
7
 
9
8
  export { AdminRouter } from "./AdminRouter.ts";
10
- // Layout
11
- export { default as AdminLayout } from "./components/AdminLayout.tsx";
12
9
  // Audits
13
10
  export { default as AdminAudits } from "./components/audits/AdminAudits.tsx";
14
11
  // Files
15
12
  export { default as AdminFiles } from "./components/files/AdminFiles.tsx";
16
13
  // Jobs
17
14
  export { default as AdminJobs } from "./components/jobs/AdminJobs.tsx";
15
+ // API Keys
16
+ export { default as AdminApiKeys } from "./components/keys/AdminApiKeys.tsx";
18
17
  // Notifications
19
18
  export { default as AdminNotifications } from "./components/notifications/AdminNotifications.tsx";
20
19
  // Parameters
@@ -31,14 +30,14 @@ export { default as AdminUserSettings } from "./components/users/AdminUserSettin
31
30
  export { default as AdminUsers } from "./components/users/AdminUsers.tsx";
32
31
  // Verifications
33
32
  export { default as AdminVerifications } from "./components/verifications/AdminVerifications.tsx";
34
- export { MainRouter } from "./MainRouter.ts";
33
+ export * from "./primitives/$uiAdmin.ts";
35
34
 
36
35
  // ---------------------------------------------------------------------------------------------------------------------
37
36
 
38
37
  /**
39
- * | type | quality | stability |
40
- * |------|---------|-----------|
41
- * | frontend | rare | experimental |
38
+ * | Stability | Since | Runtime |
39
+ * |-----------|-------|---------|
40
+ * | 2 - beta | 0.12.0 | node, bun, workerd, browser|
42
41
  *
43
42
  * Admin panel UI components.
44
43
  *
@@ -57,18 +56,10 @@ export { MainRouter } from "./MainRouter.ts";
57
56
  */
58
57
  export const AlephaUIAdmin = $module({
59
58
  name: "alepha.ui.admin",
60
- services: [AlephaUI, AlephaUIAuth, AdminRouter, MainRouter],
59
+ services: [AlephaUI, AlephaUIAuth, AdminRouter],
61
60
  register: (alepha) => {
62
61
  alepha.with(AdminRouter);
63
62
  },
64
63
  });
65
64
 
66
65
  // ---------------------------------------------------------------------------------------------------------------------
67
-
68
- /**
69
- * Register Admin UI components and get the AdminRouter instance.
70
- */
71
- export const $uiAdmin = () => {
72
- const { alepha } = $context();
73
- return alepha.inject(AdminRouter);
74
- };
@@ -0,0 +1,17 @@
1
+ import type { AdminShellProps } from "@alepha/ui";
2
+ import { $context } from "alepha";
3
+ import { AdminRouter } from "../AdminRouter.ts";
4
+
5
+ /**
6
+ * Register Admin UI components and get the AdminRouter instance.
7
+ */
8
+ export const $uiAdmin = (optsFn?: (a: AdminRouter) => AdminShellProps) => {
9
+ const { alepha } = $context();
10
+ const adminRouter = alepha.inject(AdminRouter);
11
+
12
+ if (optsFn) {
13
+ adminRouter.configFn = optsFn;
14
+ }
15
+
16
+ return adminRouter;
17
+ };
@@ -9,7 +9,7 @@ import {
9
9
  import { $inject, AlephaError, t } from "alepha";
10
10
  import type { RealmController } from "alepha/api/users";
11
11
  import { ReactAuth } from "alepha/react/auth";
12
- import { $page } from "alepha/react/router";
12
+ import { $page, Redirection } from "alepha/react/router";
13
13
  import { $client } from "alepha/server/links";
14
14
 
15
15
  /**
@@ -26,8 +26,7 @@ export class AuthRouter {
26
26
  protected readonly realmClient = $client<RealmController>();
27
27
  protected readonly auth = $inject(ReactAuth);
28
28
 
29
- layout = $page({
30
- name: "AuthLayout",
29
+ authLayout = $page({
31
30
  path: "/auth",
32
31
  lazy: () => import("./components/AuthLayout.tsx"),
33
32
  children: () => [
@@ -49,7 +48,10 @@ export class AuthRouter {
49
48
  },
50
49
  can: () => !this.auth.user,
51
50
  lazy: () => import("./components/Login.tsx"),
52
- loader: async ({ query }) => {
51
+ loader: async ({ query, user }) => {
52
+ if (user) {
53
+ throw new Redirection(query.r || "/");
54
+ }
53
55
  return {
54
56
  realmConfig: await this.loadRealmConfig(query.realm),
55
57
  };
@@ -66,7 +68,10 @@ export class AuthRouter {
66
68
  },
67
69
  can: () => !this.auth.user,
68
70
  lazy: () => import("./components/Register.tsx"),
69
- loader: async ({ query }) => {
71
+ loader: async ({ query, user }) => {
72
+ if (user) {
73
+ throw new Redirection(query.r || "/");
74
+ }
70
75
  return {
71
76
  realmConfig: await this.loadRealmConfig(query.realm),
72
77
  };
@@ -83,7 +88,10 @@ export class AuthRouter {
83
88
  },
84
89
  can: () => !this.auth.user,
85
90
  lazy: () => import("./components/ResetPassword.tsx"),
86
- loader: async ({ query }) => {
91
+ loader: async ({ query, user }) => {
92
+ if (user) {
93
+ throw new Redirection(query.r || "/");
94
+ }
87
95
  return {
88
96
  realmConfig: await this.loadRealmConfig(query.realm),
89
97
  };
@@ -1,6 +1,7 @@
1
1
  import { AlephaMantineProvider } from "@alepha/ui";
2
2
  import { Flex } from "@mantine/core";
3
3
  import { NestedView } from "alepha/react/router";
4
+ import "@alepha/ui/styles";
4
5
 
5
6
  const AuthLayout = () => {
6
7
  return (