@alepha/ui 0.15.4 → 0.15.5

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 (99) 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-BX4FIpXv.css +143 -0
  14. package/dist/admin/AdminLayout-BX4FIpXv.css.map +1 -0
  15. package/dist/admin/{AdminLayout-QJLIesuG.js → AdminLayout-BqZiXx4H.js} +3 -2
  16. package/dist/admin/AdminLayout-BqZiXx4H.js.map +1 -0
  17. package/dist/admin/AdminNotifications-B0B1rdc4.js +3 -0
  18. package/dist/admin/{AdminNotifications-CgYkBuG_.js → AdminNotifications-Ds5Un0NJ.js} +2 -2
  19. package/dist/admin/{AdminNotifications-CgYkBuG_.js.map → AdminNotifications-Ds5Un0NJ.js.map} +1 -1
  20. package/dist/admin/{AdminParameters-Cl-R0nXt.js → AdminParameters-BU3lATdJ.js} +1 -1
  21. package/dist/admin/{AdminParameters-hjNG_KXb.js → AdminParameters-CfDUpc78.js} +4 -4
  22. package/dist/admin/{AdminParameters-hjNG_KXb.js.map → AdminParameters-CfDUpc78.js.map} +1 -1
  23. package/dist/admin/AdminSessions-BDGK2MS6.js +3 -0
  24. package/dist/admin/{AdminSessions-Bey9cuy1.js → AdminSessions-DzIOxM3b.js} +2 -2
  25. package/dist/admin/{AdminSessions-Bey9cuy1.js.map → AdminSessions-DzIOxM3b.js.map} +1 -1
  26. package/dist/admin/{AdminUserAudits-C7AN9jx7.js → AdminUserAudits-CiUPN2BC.js} +2 -2
  27. package/dist/admin/{AdminUserAudits-C7AN9jx7.js.map → AdminUserAudits-CiUPN2BC.js.map} +1 -1
  28. package/dist/admin/{AdminUserAudits-Cp_ERd2g.js → AdminUserAudits-Cj79gENT.js} +1 -1
  29. package/dist/admin/{AdminUserCreate-BVIm4JdN.js → AdminUserCreate-BwQKr4xE.js} +2 -2
  30. package/dist/admin/{AdminUserCreate-BVIm4JdN.js.map → AdminUserCreate-BwQKr4xE.js.map} +1 -1
  31. package/dist/admin/{AdminUserCreate-C1aInRDk.js → AdminUserCreate-Cq-mUmBs.js} +1 -1
  32. package/dist/admin/{AdminUserDetails-Dcn3OwMC.js → AdminUserDetails-DRjVAPFd.js} +1 -1
  33. package/dist/admin/{AdminUserDetails-yM4x8JE6.js → AdminUserDetails-uqtC5aJ1.js} +2 -2
  34. package/dist/admin/{AdminUserDetails-yM4x8JE6.js.map → AdminUserDetails-uqtC5aJ1.js.map} +1 -1
  35. package/dist/admin/{AdminUserLayout-gb-nbggz.js → AdminUserLayout-CGzmHHby.js} +1 -1
  36. package/dist/admin/{AdminUserLayout-BnfBC1gD.js → AdminUserLayout-CiPay35T.js} +2 -2
  37. package/dist/admin/{AdminUserLayout-BnfBC1gD.js.map → AdminUserLayout-CiPay35T.js.map} +1 -1
  38. package/dist/admin/{AdminUserSessions-kmkXG-xf.js → AdminUserSessions-DAE8Nf1F.js} +2 -2
  39. package/dist/admin/{AdminUserSessions-kmkXG-xf.js.map → AdminUserSessions-DAE8Nf1F.js.map} +1 -1
  40. package/dist/admin/AdminUserSessions-DcdzuNZ9.js +3 -0
  41. package/dist/admin/AdminUserSettings-D7V6-ceX.js +3 -0
  42. package/dist/admin/{AdminUserSettings-DZ9iWhJW.js → AdminUserSettings-EbahaV2a.js} +2 -2
  43. package/dist/admin/{AdminUserSettings-DZ9iWhJW.js.map → AdminUserSettings-EbahaV2a.js.map} +1 -1
  44. package/dist/admin/AdminUsers-D9nyzGqQ.js +3 -0
  45. package/dist/admin/{AdminUsers-D6Y5K8Am.js → AdminUsers-Dcjh0KNW.js} +2 -2
  46. package/dist/admin/{AdminUsers-D6Y5K8Am.js.map → AdminUsers-Dcjh0KNW.js.map} +1 -1
  47. package/dist/admin/index.d.ts +24 -36
  48. package/dist/admin/index.d.ts.map +1 -1
  49. package/dist/admin/index.js +52 -169
  50. package/dist/admin/index.js.map +1 -1
  51. package/dist/auth/AuthLayout-BaD7RD2h.css +143 -0
  52. package/dist/auth/AuthLayout-BaD7RD2h.css.map +1 -0
  53. package/dist/auth/AuthLayout-Dj5K4SIN.js.map +1 -1
  54. package/dist/auth/index.d.ts +9 -1
  55. package/dist/auth/index.d.ts.map +1 -1
  56. package/dist/auth/index.js +1 -2
  57. package/dist/auth/index.js.map +1 -1
  58. package/dist/core/index.d.ts +13 -21
  59. package/dist/core/index.d.ts.map +1 -1
  60. package/dist/core/index.js +26 -38
  61. package/dist/core/index.js.map +1 -1
  62. package/dist/demo/{DemoLogin-S-b15cmE.js → DemoLogin-CvCG2WVh.js} +3 -1
  63. package/dist/demo/{DemoLogin-S-b15cmE.js.map → DemoLogin-CvCG2WVh.js.map} +1 -1
  64. package/dist/demo/{DemoRegister-B29MdAaZ.js → DemoRegister-CmeHbOAs.js} +3 -1
  65. package/dist/demo/{DemoRegister-B29MdAaZ.js.map → DemoRegister-CmeHbOAs.js.map} +1 -1
  66. package/dist/demo/{DemoResetPassword-CPTy88iK.js → DemoResetPassword-CKO5iA_6.js} +3 -1
  67. package/dist/demo/{DemoResetPassword-CPTy88iK.js.map → DemoResetPassword-CKO5iA_6.js.map} +1 -1
  68. package/dist/demo/index.js +3 -3
  69. package/package.json +3 -3
  70. package/src/admin/AdminRouter.ts +34 -0
  71. package/src/admin/components/AdminLayout.tsx +2 -0
  72. package/src/admin/components/jobs/AdminJobs.tsx +733 -119
  73. package/src/admin/components/keys/AdminApiKeys.tsx +537 -0
  74. package/src/admin/components/parameters/AdminParameters.tsx +2 -3
  75. package/src/admin/index.ts +3 -5
  76. package/src/auth/AuthRouter.ts +1 -2
  77. package/src/auth/components/AuthLayout.tsx +1 -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/AppBar.tsx +5 -8
  85. package/src/core/index.ts +0 -1
  86. package/src/core/styles.css +1 -0
  87. package/src/demo/components/auth/DemoLogin.tsx +2 -0
  88. package/src/demo/components/auth/DemoRegister.tsx +2 -0
  89. package/src/demo/components/auth/DemoResetPassword.tsx +2 -0
  90. package/dist/admin/AdminAudits-BU-p1g7A.js +0 -3
  91. package/dist/admin/AdminFiles-Bg9feLFH.js +0 -3
  92. package/dist/admin/AdminLayout-BfeFXiul.js +0 -3
  93. package/dist/admin/AdminLayout-QJLIesuG.js.map +0 -1
  94. package/dist/admin/AdminNotifications-DmfGPqHe.js +0 -3
  95. package/dist/admin/AdminSessions-Cn4_jB04.js +0 -3
  96. package/dist/admin/AdminUserSessions-rvA0ztxn.js +0 -3
  97. package/dist/admin/AdminUserSettings-Dg-wTRzN.js +0 -3
  98. package/dist/admin/AdminUsers-RCaxccEW.js +0 -3
  99. 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);
@@ -2,19 +2,18 @@ import { AlephaUI } from "@alepha/ui";
2
2
  import { AlephaUIAuth } from "@alepha/ui/auth";
3
3
  import { $context, $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,7 +30,6 @@ 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";
35
33
 
36
34
  // ---------------------------------------------------------------------------------------------------------------------
37
35
 
@@ -57,7 +55,7 @@ export { MainRouter } from "./MainRouter.ts";
57
55
  */
58
56
  export const AlephaUIAdmin = $module({
59
57
  name: "alepha.ui.admin",
60
- services: [AlephaUI, AlephaUIAuth, AdminRouter, MainRouter],
58
+ services: [AlephaUI, AlephaUIAuth, AdminRouter],
61
59
  register: (alepha) => {
62
60
  alepha.with(AdminRouter);
63
61
  },
@@ -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: () => [
@@ -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 "../../core/styles.css";
4
5
 
5
6
  const AuthLayout = () => {
6
7
  return (
@@ -567,14 +567,21 @@ const ActionNavigationButton = (props: ActionNavigationButtonProps) => {
567
567
  classNameActive,
568
568
  variantActive,
569
569
  routerGoOptions,
570
+ onClick: propsOnClick,
570
571
  ...buttonProps
571
- } = props;
572
+ } = props as ActionNavigationButtonProps & { onClick?: (e: any) => void };
572
573
  const router = useRouter();
573
574
  const { isPending, isActive } = useActive(
574
575
  options ? { href: props.href, ...options } : { href: props.href },
575
576
  );
576
577
  const anchorProps = router.anchor(props.href, routerGoOptions);
577
578
 
579
+ // Combine passed onClick with router's onClick
580
+ const combinedOnClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
581
+ propsOnClick?.(e as any);
582
+ anchorProps.onClick?.(e);
583
+ };
584
+
578
585
  const className = buttonProps.className || "";
579
586
  if (isActive && options !== false && classNameActive) {
580
587
  buttonProps.className = `${className} ${classNameActive}`.trim();
@@ -582,7 +589,12 @@ const ActionNavigationButton = (props: ActionNavigationButtonProps) => {
582
589
 
583
590
  if (props.anchorProps) {
584
591
  return (
585
- <Anchor component={"a"} {...anchorProps} {...props.anchorProps}>
592
+ <Anchor
593
+ component={"a"}
594
+ {...anchorProps}
595
+ {...props.anchorProps}
596
+ onClick={combinedOnClick}
597
+ >
586
598
  {props.children}
587
599
  </Anchor>
588
600
  );
@@ -594,6 +606,7 @@ const ActionNavigationButton = (props: ActionNavigationButtonProps) => {
594
606
  loading={isPending}
595
607
  {...buttonProps}
596
608
  {...anchorProps}
609
+ onClick={combinedOnClick}
597
610
  variant={
598
611
  isActive && options !== false
599
612
  ? (variantActive ?? "filled")
@@ -0,0 +1,6 @@
1
+ /* Dark Mode Toggle - SSR-safe icon switching */
2
+
3
+ [data-mantine-color-scheme="light"] .alepha-light-hidden,
4
+ [data-mantine-color-scheme="dark"] .alepha-dark-hidden {
5
+ display: none;
6
+ }