@alepha/ui 0.13.2 → 0.13.4

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 (122) hide show
  1. package/dist/admin/{AdminFiles-BjofP3OC.js → AdminFiles-8CC9mVsc.js} +3 -3
  2. package/dist/admin/{AdminFiles-BjofP3OC.js.map → AdminFiles-8CC9mVsc.js.map} +1 -1
  3. package/dist/admin/AdminFiles-BRLMP_7y.js +3 -0
  4. package/dist/admin/AdminLayout-Cm-Y4YTQ.js +396 -0
  5. package/dist/admin/AdminLayout-Cm-Y4YTQ.js.map +1 -0
  6. package/dist/admin/AdminLayout-D5M9kSiV.js +3 -0
  7. package/dist/admin/AdminNotifications-DxBKi2RO.js +3 -0
  8. package/dist/admin/AdminNotifications-d-gw5Uie.js +154 -0
  9. package/dist/admin/AdminNotifications-d-gw5Uie.js.map +1 -0
  10. package/dist/admin/{AdminSessions-CmDVneE2.js → AdminSessions-CpVusqmd.js} +37 -10
  11. package/dist/admin/AdminSessions-CpVusqmd.js.map +1 -0
  12. package/dist/admin/AdminSessions-DA285-5Q.js +3 -0
  13. package/dist/admin/AdminUserCreate-CQIrSslj.js +3 -0
  14. package/dist/admin/AdminUserCreate-DH7u_yJj.js +103 -0
  15. package/dist/admin/AdminUserCreate-DH7u_yJj.js.map +1 -0
  16. package/dist/admin/AdminUserDetails-DVmFCDsU.js +221 -0
  17. package/dist/admin/AdminUserDetails-DVmFCDsU.js.map +1 -0
  18. package/dist/admin/AdminUserDetails-T3nkXSdz.js +3 -0
  19. package/dist/admin/AdminUserLayout-DdtZGX8n.js +3 -0
  20. package/dist/admin/AdminUserLayout-gpOyn0Y7.js +153 -0
  21. package/dist/admin/AdminUserLayout-gpOyn0Y7.js.map +1 -0
  22. package/dist/admin/AdminUserSessions-CWYzjB3D.js +3 -0
  23. package/dist/admin/AdminUserSessions-CdVwoM-h.js +129 -0
  24. package/dist/admin/AdminUserSessions-CdVwoM-h.js.map +1 -0
  25. package/dist/admin/AdminUserSettings-S7gZvvjO.js +164 -0
  26. package/dist/admin/AdminUserSettings-S7gZvvjO.js.map +1 -0
  27. package/dist/admin/AdminUserSettings-jCzVYw_2.js +3 -0
  28. package/dist/admin/{AdminUsers-88De5pev.js → AdminUsers-9qEzxqAL.js} +33 -15
  29. package/dist/admin/AdminUsers-9qEzxqAL.js.map +1 -0
  30. package/dist/admin/AdminUsers-BcSUxV01.js +3 -0
  31. package/dist/admin/index.d.ts +5568 -418
  32. package/dist/admin/index.js +302 -42
  33. package/dist/admin/index.js.map +1 -1
  34. package/dist/auth/AuthLayout-BSL8ZHgr.js +19 -0
  35. package/dist/auth/AuthLayout-BSL8ZHgr.js.map +1 -0
  36. package/dist/auth/{Login-OCrvjs9U.js → Login-AlVPPqQp.js} +6 -5
  37. package/dist/auth/Login-AlVPPqQp.js.map +1 -0
  38. package/dist/auth/Login-otdWVvVU.js +4 -0
  39. package/dist/auth/{Register-Ei34GSba.js → Register-BxJmOqpF.js} +9 -6
  40. package/dist/auth/Register-BxJmOqpF.js.map +1 -0
  41. package/dist/auth/Register-D10MnlQc.js +4 -0
  42. package/dist/auth/{ResetPassword-tO0oMzfo.js → ResetPassword-BhyZ9ek4.js} +3 -3
  43. package/dist/auth/ResetPassword-BhyZ9ek4.js.map +1 -0
  44. package/dist/auth/ResetPassword-llBG-STp.js +3 -0
  45. package/dist/auth/VerifyEmail-BvOG-IUC.js +3 -0
  46. package/dist/auth/VerifyEmail-DeLct3oQ.js +131 -0
  47. package/dist/auth/VerifyEmail-DeLct3oQ.js.map +1 -0
  48. package/dist/auth/index.d.ts +2412 -2254
  49. package/dist/auth/index.js +97 -20
  50. package/dist/auth/index.js.map +1 -1
  51. package/dist/core/index.d.ts +280 -95
  52. package/dist/core/index.js +1375 -394
  53. package/dist/core/index.js.map +1 -1
  54. package/package.json +7 -6
  55. package/src/admin/AdminRouter.ts +116 -29
  56. package/src/admin/AdminSidebar.ts +31 -0
  57. package/src/admin/MainRouter.ts +23 -0
  58. package/src/admin/components/AdminLayout.tsx +66 -104
  59. package/src/admin/components/AdminNotifications.tsx +196 -12
  60. package/src/admin/components/AdminSessions.tsx +43 -7
  61. package/src/admin/components/AdminUserCreate.tsx +84 -0
  62. package/src/admin/components/AdminUserDetails.tsx +180 -0
  63. package/src/admin/components/AdminUserLayout.tsx +172 -0
  64. package/src/admin/components/AdminUserSessions.tsx +158 -0
  65. package/src/admin/components/AdminUserSettings.tsx +165 -0
  66. package/src/admin/components/AdminUsers.tsx +29 -9
  67. package/src/admin/index.ts +15 -3
  68. package/src/auth/AuthI18n.ts +22 -0
  69. package/src/auth/AuthRouter.ts +82 -8
  70. package/src/auth/components/AuthLayout.tsx +12 -0
  71. package/src/auth/components/Login.tsx +14 -12
  72. package/src/auth/components/Register.tsx +6 -5
  73. package/src/auth/components/ResetPassword.tsx +1 -1
  74. package/src/auth/components/VerifyEmail.tsx +102 -0
  75. package/src/auth/components/buttons/UserButton.tsx +12 -2
  76. package/src/auth/index.ts +1 -0
  77. package/src/core/components/buttons/ActionButton.tsx +12 -4
  78. package/src/core/components/buttons/DarkModeButton.tsx +1 -1
  79. package/src/core/components/buttons/ThemeButton.tsx +31 -0
  80. package/src/core/components/layout/AdminShell.tsx +4 -2
  81. package/src/core/components/layout/AlephaMantineProvider.tsx +10 -4
  82. package/src/core/components/layout/Omnibar.tsx +27 -15
  83. package/src/core/components/layout/Sidebar.tsx +33 -17
  84. package/src/core/components/table/DataTable.tsx +9 -5
  85. package/src/core/hooks/useTheme.ts +25 -0
  86. package/src/core/index.ts +8 -3
  87. package/src/core/providers/ThemeProvider.ts +90 -0
  88. package/src/core/themes/aurora.ts +107 -0
  89. package/src/core/themes/crystal.ts +107 -0
  90. package/src/core/themes/default.ts +7 -0
  91. package/src/core/themes/ember.ts +107 -0
  92. package/src/core/themes/index.ts +7 -0
  93. package/src/core/themes/midnight.ts +98 -0
  94. package/src/core/themes/remoraid.ts +278 -0
  95. package/src/core/themes/slate.ts +81 -0
  96. package/styles.css +84 -0
  97. package/dist/admin/AdminFiles-DldZB7oo.js +0 -3
  98. package/dist/admin/AdminJobs-BOq6AZOW.js +0 -3
  99. package/dist/admin/AdminJobs-CDnVxEv6.js +0 -125
  100. package/dist/admin/AdminJobs-CDnVxEv6.js.map +0 -1
  101. package/dist/admin/AdminLayout-Bgx25J8m.js +0 -3
  102. package/dist/admin/AdminLayout-CervL8LV.js +0 -88
  103. package/dist/admin/AdminLayout-CervL8LV.js.map +0 -1
  104. package/dist/admin/AdminNotifications-BDQXt3-e.js +0 -3
  105. package/dist/admin/AdminNotifications-DvI2989x.js +0 -40
  106. package/dist/admin/AdminNotifications-DvI2989x.js.map +0 -1
  107. package/dist/admin/AdminParameters-D_v0GAvI.js +0 -3
  108. package/dist/admin/AdminParameters-P1LB6ZI1.js +0 -40
  109. package/dist/admin/AdminParameters-P1LB6ZI1.js.map +0 -1
  110. package/dist/admin/AdminSessions-CmDVneE2.js.map +0 -1
  111. package/dist/admin/AdminSessions-Dkk_fzWK.js +0 -3
  112. package/dist/admin/AdminUsers-88De5pev.js.map +0 -1
  113. package/dist/admin/AdminUsers-oyAXqZ5l.js +0 -3
  114. package/dist/admin/AdminVerifications-D93TKymL.js +0 -3
  115. package/dist/admin/AdminVerifications-DBVEoqJe.js +0 -40
  116. package/dist/admin/AdminVerifications-DBVEoqJe.js.map +0 -1
  117. package/dist/auth/Login-BC2jTczq.js +0 -4
  118. package/dist/auth/Login-OCrvjs9U.js.map +0 -1
  119. package/dist/auth/Register-Dh0lsQmI.js +0 -4
  120. package/dist/auth/Register-Ei34GSba.js.map +0 -1
  121. package/dist/auth/ResetPassword-BnlAQAOE.js +0 -3
  122. package/dist/auth/ResetPassword-tO0oMzfo.js.map +0 -1
@@ -1,11 +1,12 @@
1
- import { useClient } from "@alepha/react";
1
+ import { useClient, useRouter } from "@alepha/react";
2
2
  import { useI18n } from "@alepha/react/i18n";
3
- import { DataTable, Flex, Text } from "@alepha/ui";
3
+ import { ActionButton, DataTable, Flex, Text } from "@alepha/ui";
4
4
  import { Badge, Group } from "@mantine/core";
5
5
  import {
6
6
  IconDeviceDesktop,
7
7
  IconDeviceMobile,
8
8
  IconDeviceTablet,
9
+ IconTrash,
9
10
  } from "@tabler/icons-react";
10
11
  import { type Page, t } from "alepha";
11
12
  import {
@@ -13,6 +14,8 @@ import {
13
14
  type SessionEntity,
14
15
  sessions,
15
16
  } from "alepha/api/users";
17
+ import { useState } from "react";
18
+ import type { AdminRouter } from "../AdminRouter.ts";
16
19
 
17
20
  export interface AdminSessionsProps {
18
21
  userRealmName?: string;
@@ -20,7 +23,9 @@ export interface AdminSessionsProps {
20
23
 
21
24
  const AdminSessions = (props: AdminSessionsProps) => {
22
25
  const client = useClient<SessionController>();
26
+ const router = useRouter<AdminRouter>();
23
27
  const { l } = useI18n();
28
+ const [refreshKey, setRefreshKey] = useState(0);
24
29
 
25
30
  const filters = t.object({
26
31
  userId: t.optional(
@@ -47,9 +52,18 @@ const AdminSessions = (props: AdminSessionsProps) => {
47
52
  return new Date(expiresAt) < new Date();
48
53
  };
49
54
 
55
+ const handleDelete = async (sessionId: string) => {
56
+ await client.deleteSession({
57
+ params: { id: sessionId },
58
+ query: { userRealmName: props.userRealmName },
59
+ });
60
+ setRefreshKey((k) => k + 1);
61
+ };
62
+
50
63
  return (
51
- <Flex flex={1}>
64
+ <Flex flex={1} direction="column">
52
65
  <DataTable<SessionEntity, typeof filters>
66
+ key={refreshKey}
53
67
  submitOnInit
54
68
  defaultSize={10}
55
69
  typeFormProps={{
@@ -86,11 +100,19 @@ const AdminSessions = (props: AdminSessionsProps) => {
86
100
  }}
87
101
  columns={{
88
102
  userId: {
89
- label: "User ID",
103
+ label: "User",
90
104
  value: (item) => (
91
- <Text size="xs" ff="monospace">
92
- {item.userId.slice(0, 8)}...
93
- </Text>
105
+ <ActionButton
106
+ variant="subtle"
107
+ size="xs"
108
+ href={router.path("adminUserDetails", {
109
+ params: { userId: item.userId },
110
+ })}
111
+ >
112
+ <Text size="xs" ff="monospace">
113
+ {item.userId.slice(0, 8)}...
114
+ </Text>
115
+ </ActionButton>
94
116
  ),
95
117
  },
96
118
  userAgent: {
@@ -150,6 +172,20 @@ const AdminSessions = (props: AdminSessionsProps) => {
150
172
  </Text>
151
173
  ),
152
174
  },
175
+ actions: {
176
+ label: "",
177
+ fit: true,
178
+ value: (item) => (
179
+ <ActionButton
180
+ size="xs"
181
+ variant="subtle"
182
+ color="red"
183
+ onClick={() => handleDelete(item.id)}
184
+ >
185
+ <IconTrash size={14} />
186
+ </ActionButton>
187
+ ),
188
+ },
153
189
  }}
154
190
  />
155
191
  </Flex>
@@ -0,0 +1,84 @@
1
+ import { useClient, useRouter } from "@alepha/react";
2
+ import { useForm } from "@alepha/react/form";
3
+ import { ActionButton, Control, Flex } from "@alepha/ui";
4
+ import { Card, Stack, Text } from "@mantine/core";
5
+ import { t } from "alepha";
6
+ import type { UserController } from "alepha/api/users";
7
+ import type { AdminRouter } from "../AdminRouter.ts";
8
+
9
+ export interface AdminUserCreateProps {
10
+ userRealmName?: string;
11
+ }
12
+
13
+ const AdminUserCreate = (props: AdminUserCreateProps) => {
14
+ const client = useClient<UserController>();
15
+ const router = useRouter<AdminRouter>();
16
+
17
+ const form = useForm({
18
+ schema: t.object({
19
+ username: t.optional(
20
+ t.shortText({
21
+ minLength: 3,
22
+ maxLength: 50,
23
+ pattern: "^[a-zA-Z0-9._-]+$",
24
+ }),
25
+ ),
26
+ email: t.optional(t.email()),
27
+ phoneNumber: t.optional(t.e164()),
28
+ firstName: t.optional(t.string()),
29
+ lastName: t.optional(t.string()),
30
+ roles: t.optional(t.array(t.string())),
31
+ enabled: t.optional(t.boolean()),
32
+ password: t.optional(t.string({ minLength: 8 })),
33
+ }),
34
+ handler: async (data) => {
35
+ const user = await client.createUser({
36
+ query: {
37
+ userRealmName: props.userRealmName,
38
+ },
39
+ body: {
40
+ ...data,
41
+ enabled: data.enabled ?? true,
42
+ },
43
+ });
44
+
45
+ await router.go("adminUserDetails", {
46
+ params: { userId: user.id },
47
+ });
48
+ },
49
+ });
50
+
51
+ return (
52
+ <Flex flex={1} p="md">
53
+ <Card withBorder p="lg" maw={600} w="100%">
54
+ <form {...form.props}>
55
+ <Stack gap="md">
56
+ <Text size="lg" fw={500}>
57
+ Create New User
58
+ </Text>
59
+
60
+ <Control title="Username" input={form.input.username} />
61
+
62
+ <Control title="Email" input={form.input.email} />
63
+
64
+ <Control title="Phone Number" input={form.input.phoneNumber} />
65
+
66
+ <Control title="First Name" input={form.input.firstName} />
67
+
68
+ <Control title="Last Name" input={form.input.lastName} />
69
+
70
+ <Control title="Password" input={form.input.password} password />
71
+
72
+ <Control title="Roles" input={form.input.roles} />
73
+
74
+ <Control title="Enabled" input={form.input.enabled} />
75
+
76
+ <ActionButton form={form}>Create User</ActionButton>
77
+ </Stack>
78
+ </form>
79
+ </Card>
80
+ </Flex>
81
+ );
82
+ };
83
+
84
+ export default AdminUserCreate;
@@ -0,0 +1,180 @@
1
+ import { useClient, useRouterState } from "@alepha/react";
2
+ import { useForm } from "@alepha/react/form";
3
+ import { useI18n } from "@alepha/react/i18n";
4
+ import { ActionButton, Control, Flex, Text } from "@alepha/ui";
5
+ import { Card, Group, Loader, Stack } from "@mantine/core";
6
+ import { IconCheck, IconX } from "@tabler/icons-react";
7
+ import { t } from "alepha";
8
+ import type { UserController, UserEntity } from "alepha/api/users";
9
+ import { useEffect, useState } from "react";
10
+
11
+ export interface AdminUserDetailsProps {
12
+ userRealmName?: string;
13
+ }
14
+
15
+ const AdminUserDetails = (props: AdminUserDetailsProps) => {
16
+ const state = useRouterState();
17
+ const client = useClient<UserController>();
18
+ const { l } = useI18n();
19
+ const userId = state.params.userId as string;
20
+
21
+ const [user, setUser] = useState<UserEntity | null>(null);
22
+ const [loading, setLoading] = useState(true);
23
+
24
+ useEffect(() => {
25
+ const loadUser = async () => {
26
+ try {
27
+ const data = await client.getUser({
28
+ params: { id: userId },
29
+ query: { userRealmName: props.userRealmName },
30
+ });
31
+ setUser(data);
32
+ } finally {
33
+ setLoading(false);
34
+ }
35
+ };
36
+
37
+ loadUser();
38
+ }, [userId]);
39
+
40
+ const form = useForm({
41
+ schema: t.object({
42
+ email: t.optional(t.email()),
43
+ phoneNumber: t.optional(t.e164()),
44
+ firstName: t.optional(t.string()),
45
+ lastName: t.optional(t.string()),
46
+ roles: t.optional(t.array(t.string())),
47
+ enabled: t.optional(t.boolean()),
48
+ }),
49
+ handler: async (data) => {
50
+ const updated = await client.updateUser({
51
+ params: { id: userId },
52
+ query: { userRealmName: props.userRealmName },
53
+ body: data,
54
+ });
55
+ setUser(updated);
56
+ },
57
+ });
58
+
59
+ useEffect(() => {
60
+ if (user) {
61
+ form.input.email?.set(user.email ?? "");
62
+ form.input.phoneNumber?.set(user.phoneNumber ?? "");
63
+ form.input.firstName?.set(user.firstName ?? "");
64
+ form.input.lastName?.set(user.lastName ?? "");
65
+ form.input.roles?.set(user.roles ?? []);
66
+ form.input.enabled?.set(user.enabled);
67
+ }
68
+ }, [user]);
69
+
70
+ if (loading) {
71
+ return (
72
+ <Flex flex={1} justify="center" align="center">
73
+ <Loader />
74
+ </Flex>
75
+ );
76
+ }
77
+
78
+ if (!user) {
79
+ return (
80
+ <Flex flex={1} justify="center" align="center">
81
+ <Text c="dimmed">User not found</Text>
82
+ </Flex>
83
+ );
84
+ }
85
+
86
+ return (
87
+ <Flex flex={1} direction="column" gap="md">
88
+ <Card withBorder p="lg">
89
+ <Stack gap="md">
90
+ <Text size="lg" fw={500}>
91
+ User Details
92
+ </Text>
93
+
94
+ <Group gap="xl">
95
+ <Stack gap={4}>
96
+ <Text size="xs" c="dimmed">
97
+ User ID
98
+ </Text>
99
+ <Text size="sm" ff="monospace">
100
+ {user.id}
101
+ </Text>
102
+ </Stack>
103
+
104
+ <Stack gap={4}>
105
+ <Text size="xs" c="dimmed">
106
+ Username
107
+ </Text>
108
+ <Text size="sm">{user.username || "-"}</Text>
109
+ </Stack>
110
+
111
+ <Stack gap={4}>
112
+ <Text size="xs" c="dimmed">
113
+ Email Verified
114
+ </Text>
115
+ {user.emailVerified ? (
116
+ <Group gap={4}>
117
+ <IconCheck size={14} color="var(--mantine-color-green-6)" />
118
+ <Text size="sm" c="green">
119
+ Verified
120
+ </Text>
121
+ </Group>
122
+ ) : (
123
+ <Group gap={4}>
124
+ <IconX size={14} color="var(--mantine-color-red-6)" />
125
+ <Text size="sm" c="red">
126
+ Not Verified
127
+ </Text>
128
+ </Group>
129
+ )}
130
+ </Stack>
131
+
132
+ <Stack gap={4}>
133
+ <Text size="xs" c="dimmed">
134
+ Created
135
+ </Text>
136
+ <Text size="sm">{l(user.createdAt, { date: "medium" })}</Text>
137
+ </Stack>
138
+
139
+ <Stack gap={4}>
140
+ <Text size="xs" c="dimmed">
141
+ Updated
142
+ </Text>
143
+ <Text size="sm">{l(user.updatedAt, { date: "medium" })}</Text>
144
+ </Stack>
145
+ </Group>
146
+ </Stack>
147
+ </Card>
148
+
149
+ <Card withBorder p="lg">
150
+ <form {...form.props}>
151
+ <Stack gap="md">
152
+ <Text size="lg" fw={500}>
153
+ Edit User
154
+ </Text>
155
+
156
+ <Group grow>
157
+ <Control title="Email" input={form.input.email} />
158
+ <Control title="Phone Number" input={form.input.phoneNumber} />
159
+ </Group>
160
+
161
+ <Group grow>
162
+ <Control title="First Name" input={form.input.firstName} />
163
+ <Control title="Last Name" input={form.input.lastName} />
164
+ </Group>
165
+
166
+ <Control title="Roles" input={form.input.roles} />
167
+
168
+ <Control title="Enabled" input={form.input.enabled} />
169
+
170
+ <Group>
171
+ <ActionButton form={form}>Save Changes</ActionButton>
172
+ </Group>
173
+ </Stack>
174
+ </form>
175
+ </Card>
176
+ </Flex>
177
+ );
178
+ };
179
+
180
+ export default AdminUserDetails;
@@ -0,0 +1,172 @@
1
+ import {
2
+ NestedView,
3
+ useClient,
4
+ useRouter,
5
+ useRouterState,
6
+ } from "@alepha/react";
7
+ import { ActionButton, Flex, Text } from "@alepha/ui";
8
+ import { Avatar, Badge, Card, Group, Loader, Stack, Tabs } from "@mantine/core";
9
+ import { IconDevices, IconSettings, IconUser } from "@tabler/icons-react";
10
+ import type { UserController, UserEntity } from "alepha/api/users";
11
+ import { useEffect, useState } from "react";
12
+ import type { AdminRouter } from "../AdminRouter.ts";
13
+
14
+ export interface AdminUserLayoutProps {
15
+ userRealmName?: string;
16
+ }
17
+
18
+ const AdminUserLayout = (props: AdminUserLayoutProps) => {
19
+ const router = useRouter<AdminRouter>();
20
+ const state = useRouterState();
21
+ const client = useClient<UserController>();
22
+ const userId = state.params.userId as string;
23
+
24
+ const [user, setUser] = useState<UserEntity | null>(null);
25
+ const [loading, setLoading] = useState(true);
26
+
27
+ useEffect(() => {
28
+ const loadUser = async () => {
29
+ try {
30
+ const data = await client.getUser({
31
+ params: { id: userId },
32
+ query: { userRealmName: props.userRealmName },
33
+ });
34
+ setUser(data);
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+
40
+ loadUser();
41
+ }, [userId]);
42
+
43
+ if (loading) {
44
+ return (
45
+ <Flex flex={1} justify="center" align="center">
46
+ <Loader />
47
+ </Flex>
48
+ );
49
+ }
50
+
51
+ if (!user) {
52
+ return (
53
+ <Flex flex={1} justify="center" align="center">
54
+ <Text c="dimmed">User not found</Text>
55
+ </Flex>
56
+ );
57
+ }
58
+
59
+ const currentPath = state.url.pathname;
60
+ const detailsPath = router.path("adminUserDetails", { params: { userId } });
61
+ const sessionsPath = router.path("adminUserSessions", { params: { userId } });
62
+ const settingsPath = router.path("adminUserSettings", { params: { userId } });
63
+
64
+ const getActiveTab = () => {
65
+ if (currentPath.endsWith("/sessions")) return "sessions";
66
+ if (currentPath.endsWith("/settings")) return "settings";
67
+ return "details";
68
+ };
69
+ const activeTab = getActiveTab();
70
+
71
+ const displayName =
72
+ user.firstName || user.lastName
73
+ ? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim()
74
+ : user.username || user.email || "User";
75
+
76
+ return (
77
+ <Flex flex={1} direction="column" gap="md" p="md">
78
+ <Card withBorder p="md">
79
+ <Group>
80
+ <Avatar size="lg" radius="xl" color="blue">
81
+ {displayName.charAt(0).toUpperCase()}
82
+ </Avatar>
83
+ <Stack gap={4}>
84
+ <Group gap="xs">
85
+ <Text size="lg" fw={500}>
86
+ {displayName}
87
+ </Text>
88
+ <Badge
89
+ size="sm"
90
+ variant="light"
91
+ color={user.enabled ? "green" : "red"}
92
+ >
93
+ {user.enabled ? "Active" : "Disabled"}
94
+ </Badge>
95
+ </Group>
96
+ <Text size="sm" c="dimmed">
97
+ {user.email || user.username || user.id}
98
+ </Text>
99
+ {user.roles.length > 0 && (
100
+ <Group gap={4}>
101
+ {user.roles.map((role: string) => (
102
+ <Badge key={role} size="xs" variant="outline">
103
+ {role}
104
+ </Badge>
105
+ ))}
106
+ </Group>
107
+ )}
108
+ </Stack>
109
+ </Group>
110
+ </Card>
111
+
112
+ <Tabs value={activeTab}>
113
+ <Tabs.List>
114
+ <ActionButton
115
+ variant="subtle"
116
+ href={detailsPath}
117
+ leftSection={<IconUser size={16} />}
118
+ c={activeTab === "details" ? undefined : "dimmed"}
119
+ fw={activeTab === "details" ? 500 : 400}
120
+ style={{
121
+ borderBottom:
122
+ activeTab === "details"
123
+ ? "2px solid var(--mantine-primary-color-filled)"
124
+ : "2px solid transparent",
125
+ borderRadius: 0,
126
+ }}
127
+ >
128
+ Details
129
+ </ActionButton>
130
+ <ActionButton
131
+ variant="subtle"
132
+ href={sessionsPath}
133
+ leftSection={<IconDevices size={16} />}
134
+ c={activeTab === "sessions" ? undefined : "dimmed"}
135
+ fw={activeTab === "sessions" ? 500 : 400}
136
+ style={{
137
+ borderBottom:
138
+ activeTab === "sessions"
139
+ ? "2px solid var(--mantine-primary-color-filled)"
140
+ : "2px solid transparent",
141
+ borderRadius: 0,
142
+ }}
143
+ >
144
+ Sessions
145
+ </ActionButton>
146
+ <ActionButton
147
+ variant="subtle"
148
+ href={settingsPath}
149
+ leftSection={<IconSettings size={16} />}
150
+ c={activeTab === "settings" ? undefined : "dimmed"}
151
+ fw={activeTab === "settings" ? 500 : 400}
152
+ style={{
153
+ borderBottom:
154
+ activeTab === "settings"
155
+ ? "2px solid var(--mantine-primary-color-filled)"
156
+ : "2px solid transparent",
157
+ borderRadius: 0,
158
+ }}
159
+ >
160
+ Settings
161
+ </ActionButton>
162
+ </Tabs.List>
163
+ </Tabs>
164
+
165
+ <Flex flex={1}>
166
+ <NestedView />
167
+ </Flex>
168
+ </Flex>
169
+ );
170
+ };
171
+
172
+ export default AdminUserLayout;
@@ -0,0 +1,158 @@
1
+ import { useClient, useRouterState } from "@alepha/react";
2
+ import { useI18n } from "@alepha/react/i18n";
3
+ import { ActionButton, DataTable, Flex, Text } from "@alepha/ui";
4
+ import { Badge, Group } from "@mantine/core";
5
+ import {
6
+ IconDeviceDesktop,
7
+ IconDeviceMobile,
8
+ IconDeviceTablet,
9
+ IconTrash,
10
+ } from "@tabler/icons-react";
11
+ import { type Page, t } from "alepha";
12
+ import type { SessionController, SessionEntity } from "alepha/api/users";
13
+ import { useState } from "react";
14
+
15
+ export interface AdminUserSessionsProps {
16
+ userRealmName?: string;
17
+ }
18
+
19
+ const AdminUserSessions = (props: AdminUserSessionsProps) => {
20
+ const state = useRouterState();
21
+ const client = useClient<SessionController>();
22
+ const { l } = useI18n();
23
+ const userId = state.params.userId as string;
24
+ const [refreshKey, setRefreshKey] = useState(0);
25
+
26
+ const getDeviceIcon = (device?: string) => {
27
+ switch (device) {
28
+ case "MOBILE":
29
+ return <IconDeviceMobile size={14} />;
30
+ case "TABLET":
31
+ return <IconDeviceTablet size={14} />;
32
+ default:
33
+ return <IconDeviceDesktop size={14} />;
34
+ }
35
+ };
36
+
37
+ const isExpired = (expiresAt: Date | string) => {
38
+ return new Date(expiresAt) < new Date();
39
+ };
40
+
41
+ const handleDelete = async (sessionId: string) => {
42
+ await client.deleteSession({
43
+ params: { id: sessionId },
44
+ query: { userRealmName: props.userRealmName },
45
+ });
46
+ setRefreshKey((k) => k + 1);
47
+ };
48
+
49
+ const filters = t.object({});
50
+
51
+ return (
52
+ <Flex flex={1} direction="column">
53
+ <DataTable<SessionEntity, typeof filters>
54
+ key={refreshKey}
55
+ submitOnInit
56
+ defaultSize={10}
57
+ filters={filters}
58
+ tableProps={{
59
+ horizontalSpacing: "xs",
60
+ verticalSpacing: "xs",
61
+ }}
62
+ tableTrProps={(item) => {
63
+ if (isExpired(item.expiresAt)) {
64
+ return {
65
+ opacity: 0.5,
66
+ };
67
+ }
68
+ return {};
69
+ }}
70
+ items={async (filters) => {
71
+ const response = await client.findSessions({
72
+ query: {
73
+ ...filters,
74
+ userId,
75
+ userRealmName: props.userRealmName,
76
+ },
77
+ });
78
+
79
+ return response as Page<SessionEntity>;
80
+ }}
81
+ columns={{
82
+ userAgent: {
83
+ label: "Device",
84
+ value: (item) => (
85
+ <Group gap={4}>
86
+ {item.userAgent ? (
87
+ <>
88
+ <Badge
89
+ size="xs"
90
+ variant="light"
91
+ leftSection={getDeviceIcon(item.userAgent.device)}
92
+ >
93
+ {item.userAgent.device}
94
+ </Badge>
95
+ <Text size="xs" c="dimmed">
96
+ {item.userAgent.browser} / {item.userAgent.os}
97
+ </Text>
98
+ </>
99
+ ) : (
100
+ <Text size="xs" c="dimmed">
101
+ -
102
+ </Text>
103
+ )}
104
+ </Group>
105
+ ),
106
+ },
107
+ ip: {
108
+ label: "IP Address",
109
+ fit: true,
110
+ value: (item) => (
111
+ <Text size="xs" ff="monospace" c="dimmed">
112
+ {item.ip || "-"}
113
+ </Text>
114
+ ),
115
+ },
116
+ expiresAt: {
117
+ label: "Status",
118
+ fit: true,
119
+ value: (item) => (
120
+ <Badge
121
+ size="sm"
122
+ variant="light"
123
+ color={isExpired(item.expiresAt) ? "red" : "green"}
124
+ >
125
+ {isExpired(item.expiresAt) ? "Expired" : "Active"}
126
+ </Badge>
127
+ ),
128
+ },
129
+ createdAt: {
130
+ label: "Created",
131
+ fit: true,
132
+ value: (item) => (
133
+ <Text size="xs" c="dimmed">
134
+ {l(item.createdAt, { date: "fromNow" })}
135
+ </Text>
136
+ ),
137
+ },
138
+ actions: {
139
+ label: "",
140
+ fit: true,
141
+ value: (item) => (
142
+ <ActionButton
143
+ size="xs"
144
+ variant="subtle"
145
+ color="red"
146
+ onClick={() => handleDelete(item.id)}
147
+ >
148
+ <IconTrash size={14} />
149
+ </ActionButton>
150
+ ),
151
+ },
152
+ }}
153
+ />
154
+ </Flex>
155
+ );
156
+ };
157
+
158
+ export default AdminUserSessions;