@alepha/ui 0.13.2 → 0.13.3
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.
- package/dist/admin/AdminLayout-JakF7ESb.js +388 -0
- package/dist/admin/AdminLayout-JakF7ESb.js.map +1 -0
- package/dist/admin/AdminLayout-qNsIyl30.js +3 -0
- package/dist/admin/AdminNotifications-BPrxALdS.js +154 -0
- package/dist/admin/AdminNotifications-BPrxALdS.js.map +1 -0
- package/dist/admin/AdminNotifications-DV-35Fi3.js +3 -0
- package/dist/admin/{AdminSessions-CmDVneE2.js → AdminSessions-CMmBtbSw.js} +36 -9
- package/dist/admin/AdminSessions-CMmBtbSw.js.map +1 -0
- package/dist/admin/AdminSessions-Df2VYzlE.js +3 -0
- package/dist/admin/AdminUserCreate-Coa_yi6m.js +103 -0
- package/dist/admin/AdminUserCreate-Coa_yi6m.js.map +1 -0
- package/dist/admin/AdminUserCreate-DjiCcAk0.js +3 -0
- package/dist/admin/AdminUserDetails-BCFwOm9w.js +221 -0
- package/dist/admin/AdminUserDetails-BCFwOm9w.js.map +1 -0
- package/dist/admin/AdminUserDetails-C5yeJNa3.js +3 -0
- package/dist/admin/AdminUserLayout-B8ga5QvP.js +3 -0
- package/dist/admin/AdminUserLayout-CR2OqV9Z.js +153 -0
- package/dist/admin/AdminUserLayout-CR2OqV9Z.js.map +1 -0
- package/dist/admin/AdminUserSessions-A_5KkqTY.js +3 -0
- package/dist/admin/AdminUserSessions-Bcf6-rjG.js +129 -0
- package/dist/admin/AdminUserSessions-Bcf6-rjG.js.map +1 -0
- package/dist/admin/AdminUserSettings-DAsAhFjX.js +3 -0
- package/dist/admin/AdminUserSettings-DRYVdW6S.js +164 -0
- package/dist/admin/AdminUserSettings-DRYVdW6S.js.map +1 -0
- package/dist/admin/AdminUsers-Dd9a5UqO.js +3 -0
- package/dist/admin/{AdminUsers-88De5pev.js → AdminUsers-IN_2yHKt.js} +32 -14
- package/dist/admin/AdminUsers-IN_2yHKt.js.map +1 -0
- package/dist/admin/index.d.ts +5560 -416
- package/dist/admin/index.js +299 -41
- package/dist/admin/index.js.map +1 -1
- package/dist/auth/AuthLayout-BSL8ZHgr.js +19 -0
- package/dist/auth/AuthLayout-BSL8ZHgr.js.map +1 -0
- package/dist/auth/Login-DDsyCNAA.js +4 -0
- package/dist/auth/{Login-OCrvjs9U.js → Login-kBfaRgKG.js} +5 -4
- package/dist/auth/Login-kBfaRgKG.js.map +1 -0
- package/dist/auth/{Register-Ei34GSba.js → Register-BxJmOqpF.js} +9 -6
- package/dist/auth/Register-BxJmOqpF.js.map +1 -0
- package/dist/auth/Register-D10MnlQc.js +4 -0
- package/dist/auth/{ResetPassword-tO0oMzfo.js → ResetPassword-BhyZ9ek4.js} +3 -3
- package/dist/auth/ResetPassword-BhyZ9ek4.js.map +1 -0
- package/dist/auth/ResetPassword-llBG-STp.js +3 -0
- package/dist/auth/VerifyEmail-BvOG-IUC.js +3 -0
- package/dist/auth/VerifyEmail-DeLct3oQ.js +131 -0
- package/dist/auth/VerifyEmail-DeLct3oQ.js.map +1 -0
- package/dist/auth/index.d.ts +2412 -2254
- package/dist/auth/index.js +96 -20
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +280 -95
- package/dist/core/index.js +1381 -392
- package/dist/core/index.js.map +1 -1
- package/package.json +5 -5
- package/src/admin/AdminRouter.ts +116 -29
- package/src/admin/MainRouter.ts +23 -0
- package/src/admin/components/AdminLayout.tsx +86 -103
- package/src/admin/components/AdminNotifications.tsx +196 -12
- package/src/admin/components/AdminSessions.tsx +43 -7
- package/src/admin/components/AdminUserCreate.tsx +84 -0
- package/src/admin/components/AdminUserDetails.tsx +180 -0
- package/src/admin/components/AdminUserLayout.tsx +172 -0
- package/src/admin/components/AdminUserSessions.tsx +158 -0
- package/src/admin/components/AdminUserSettings.tsx +165 -0
- package/src/admin/components/AdminUsers.tsx +29 -9
- package/src/admin/index.ts +12 -3
- package/src/auth/AuthI18n.ts +22 -0
- package/src/auth/AuthRouter.ts +82 -8
- package/src/auth/components/AuthLayout.tsx +12 -0
- package/src/auth/components/Login.tsx +13 -11
- package/src/auth/components/Register.tsx +6 -5
- package/src/auth/components/ResetPassword.tsx +1 -1
- package/src/auth/components/VerifyEmail.tsx +102 -0
- package/src/auth/components/buttons/UserButton.tsx +6 -2
- package/src/auth/index.ts +1 -0
- package/src/core/components/buttons/ActionButton.tsx +11 -4
- package/src/core/components/buttons/DarkModeButton.tsx +1 -1
- package/src/core/components/buttons/ThemeButton.tsx +31 -0
- package/src/core/components/layout/AdminShell.tsx +4 -2
- package/src/core/components/layout/AlephaMantineProvider.tsx +10 -4
- package/src/core/components/layout/Omnibar.tsx +27 -15
- package/src/core/components/layout/Sidebar.tsx +33 -15
- package/src/core/components/table/DataTable.tsx +9 -5
- package/src/core/hooks/useTheme.ts +25 -0
- package/src/core/index.ts +8 -3
- package/src/core/providers/ThemeProvider.ts +87 -0
- package/src/core/themes/aurora.ts +107 -0
- package/src/core/themes/crystal.ts +107 -0
- package/src/core/themes/default.ts +7 -0
- package/src/core/themes/ember.ts +107 -0
- package/src/core/themes/index.ts +7 -0
- package/src/core/themes/midnight.ts +104 -0
- package/src/core/themes/remoraid.ts +278 -0
- package/src/core/themes/slate.ts +81 -0
- package/dist/admin/AdminJobs-BOq6AZOW.js +0 -3
- package/dist/admin/AdminJobs-CDnVxEv6.js +0 -125
- package/dist/admin/AdminJobs-CDnVxEv6.js.map +0 -1
- package/dist/admin/AdminLayout-Bgx25J8m.js +0 -3
- package/dist/admin/AdminLayout-CervL8LV.js +0 -88
- package/dist/admin/AdminLayout-CervL8LV.js.map +0 -1
- package/dist/admin/AdminNotifications-BDQXt3-e.js +0 -3
- package/dist/admin/AdminNotifications-DvI2989x.js +0 -40
- package/dist/admin/AdminNotifications-DvI2989x.js.map +0 -1
- package/dist/admin/AdminParameters-D_v0GAvI.js +0 -3
- package/dist/admin/AdminParameters-P1LB6ZI1.js +0 -40
- package/dist/admin/AdminParameters-P1LB6ZI1.js.map +0 -1
- package/dist/admin/AdminSessions-CmDVneE2.js.map +0 -1
- package/dist/admin/AdminSessions-Dkk_fzWK.js +0 -3
- package/dist/admin/AdminUsers-88De5pev.js.map +0 -1
- package/dist/admin/AdminUsers-oyAXqZ5l.js +0 -3
- package/dist/admin/AdminVerifications-D93TKymL.js +0 -3
- package/dist/admin/AdminVerifications-DBVEoqJe.js +0 -40
- package/dist/admin/AdminVerifications-DBVEoqJe.js.map +0 -1
- package/dist/auth/Login-BC2jTczq.js +0 -4
- package/dist/auth/Login-OCrvjs9U.js.map +0 -1
- package/dist/auth/Register-Dh0lsQmI.js +0 -4
- package/dist/auth/Register-Ei34GSba.js.map +0 -1
- package/dist/auth/ResetPassword-BnlAQAOE.js +0 -3
- 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
|
|
103
|
+
label: "User",
|
|
90
104
|
value: (item) => (
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
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;
|