@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.
- package/dist/admin/{AdminFiles-BjofP3OC.js → AdminFiles-8CC9mVsc.js} +3 -3
- package/dist/admin/{AdminFiles-BjofP3OC.js.map → AdminFiles-8CC9mVsc.js.map} +1 -1
- package/dist/admin/AdminFiles-BRLMP_7y.js +3 -0
- package/dist/admin/AdminLayout-Cm-Y4YTQ.js +396 -0
- package/dist/admin/AdminLayout-Cm-Y4YTQ.js.map +1 -0
- package/dist/admin/AdminLayout-D5M9kSiV.js +3 -0
- package/dist/admin/AdminNotifications-DxBKi2RO.js +3 -0
- package/dist/admin/AdminNotifications-d-gw5Uie.js +154 -0
- package/dist/admin/AdminNotifications-d-gw5Uie.js.map +1 -0
- package/dist/admin/{AdminSessions-CmDVneE2.js → AdminSessions-CpVusqmd.js} +37 -10
- package/dist/admin/AdminSessions-CpVusqmd.js.map +1 -0
- package/dist/admin/AdminSessions-DA285-5Q.js +3 -0
- package/dist/admin/AdminUserCreate-CQIrSslj.js +3 -0
- package/dist/admin/AdminUserCreate-DH7u_yJj.js +103 -0
- package/dist/admin/AdminUserCreate-DH7u_yJj.js.map +1 -0
- package/dist/admin/AdminUserDetails-DVmFCDsU.js +221 -0
- package/dist/admin/AdminUserDetails-DVmFCDsU.js.map +1 -0
- package/dist/admin/AdminUserDetails-T3nkXSdz.js +3 -0
- package/dist/admin/AdminUserLayout-DdtZGX8n.js +3 -0
- package/dist/admin/AdminUserLayout-gpOyn0Y7.js +153 -0
- package/dist/admin/AdminUserLayout-gpOyn0Y7.js.map +1 -0
- package/dist/admin/AdminUserSessions-CWYzjB3D.js +3 -0
- package/dist/admin/AdminUserSessions-CdVwoM-h.js +129 -0
- package/dist/admin/AdminUserSessions-CdVwoM-h.js.map +1 -0
- package/dist/admin/AdminUserSettings-S7gZvvjO.js +164 -0
- package/dist/admin/AdminUserSettings-S7gZvvjO.js.map +1 -0
- package/dist/admin/AdminUserSettings-jCzVYw_2.js +3 -0
- package/dist/admin/{AdminUsers-88De5pev.js → AdminUsers-9qEzxqAL.js} +33 -15
- package/dist/admin/AdminUsers-9qEzxqAL.js.map +1 -0
- package/dist/admin/AdminUsers-BcSUxV01.js +3 -0
- package/dist/admin/index.d.ts +5568 -418
- package/dist/admin/index.js +302 -42
- 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-OCrvjs9U.js → Login-AlVPPqQp.js} +6 -5
- package/dist/auth/Login-AlVPPqQp.js.map +1 -0
- package/dist/auth/Login-otdWVvVU.js +4 -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 +97 -20
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +280 -95
- package/dist/core/index.js +1375 -394
- package/dist/core/index.js.map +1 -1
- package/package.json +7 -6
- package/src/admin/AdminRouter.ts +116 -29
- package/src/admin/AdminSidebar.ts +31 -0
- package/src/admin/MainRouter.ts +23 -0
- package/src/admin/components/AdminLayout.tsx +66 -104
- 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 +15 -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 +14 -12
- 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 +12 -2
- package/src/auth/index.ts +1 -0
- package/src/core/components/buttons/ActionButton.tsx +12 -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 -17
- 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 +90 -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 +98 -0
- package/src/core/themes/remoraid.ts +278 -0
- package/src/core/themes/slate.ts +81 -0
- package/styles.css +84 -0
- package/dist/admin/AdminFiles-DldZB7oo.js +0 -3
- 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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { useClient, useRouter, useRouterState } from "@alepha/react";
|
|
2
|
+
import { ActionButton, Flex, Text } from "@alepha/ui";
|
|
3
|
+
import { Alert, Card, Group, Loader, Stack } from "@mantine/core";
|
|
4
|
+
import {
|
|
5
|
+
IconAlertCircle,
|
|
6
|
+
IconCheck,
|
|
7
|
+
IconMail,
|
|
8
|
+
IconTrash,
|
|
9
|
+
} 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 AdminUserSettingsProps {
|
|
15
|
+
userRealmName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const AdminUserSettings = (props: AdminUserSettingsProps) => {
|
|
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
|
+
const [deleteLoading, setDeleteLoading] = useState(false);
|
|
27
|
+
const [verifyLoading, setVerifyLoading] = useState(false);
|
|
28
|
+
const [verifySuccess, setVerifySuccess] = useState(false);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const loadUser = async () => {
|
|
32
|
+
try {
|
|
33
|
+
const data = await client.getUser({
|
|
34
|
+
params: { id: userId },
|
|
35
|
+
query: { userRealmName: props.userRealmName },
|
|
36
|
+
});
|
|
37
|
+
setUser(data);
|
|
38
|
+
} finally {
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
loadUser();
|
|
44
|
+
}, [userId]);
|
|
45
|
+
|
|
46
|
+
const handleDelete = async () => {
|
|
47
|
+
if (!confirm("Are you sure you want to delete this user?")) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setDeleteLoading(true);
|
|
52
|
+
try {
|
|
53
|
+
await client.deleteUser({
|
|
54
|
+
params: { id: userId },
|
|
55
|
+
query: { userRealmName: props.userRealmName },
|
|
56
|
+
});
|
|
57
|
+
await router.go("adminUsers");
|
|
58
|
+
} finally {
|
|
59
|
+
setDeleteLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleTriggerEmailVerification = async () => {
|
|
64
|
+
if (!user?.email) return;
|
|
65
|
+
|
|
66
|
+
setVerifyLoading(true);
|
|
67
|
+
setVerifySuccess(false);
|
|
68
|
+
try {
|
|
69
|
+
await client.requestEmailVerification({
|
|
70
|
+
query: {
|
|
71
|
+
userRealmName: props.userRealmName,
|
|
72
|
+
method: "link",
|
|
73
|
+
verifyUrl: `${window.location.origin}/verify-email`,
|
|
74
|
+
},
|
|
75
|
+
body: { email: user.email },
|
|
76
|
+
});
|
|
77
|
+
setVerifySuccess(true);
|
|
78
|
+
} finally {
|
|
79
|
+
setVerifyLoading(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (loading) {
|
|
84
|
+
return (
|
|
85
|
+
<Flex flex={1} justify="center" align="center">
|
|
86
|
+
<Loader />
|
|
87
|
+
</Flex>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!user) {
|
|
92
|
+
return (
|
|
93
|
+
<Flex flex={1} justify="center" align="center">
|
|
94
|
+
<Text c="dimmed">User not found</Text>
|
|
95
|
+
</Flex>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Flex flex={1} direction="column" gap="md">
|
|
101
|
+
{user.email && !user.emailVerified && (
|
|
102
|
+
<Card withBorder p="lg">
|
|
103
|
+
<Stack gap="md">
|
|
104
|
+
<Text size="lg" fw={500}>
|
|
105
|
+
Email Verification
|
|
106
|
+
</Text>
|
|
107
|
+
|
|
108
|
+
<Alert variant="light" color="yellow" icon={<IconMail />}>
|
|
109
|
+
<Text size="sm">
|
|
110
|
+
This user's email ({user.email}) is not verified. You can send a
|
|
111
|
+
verification link to the user.
|
|
112
|
+
</Text>
|
|
113
|
+
</Alert>
|
|
114
|
+
|
|
115
|
+
{verifySuccess && (
|
|
116
|
+
<Alert variant="light" color="green" icon={<IconCheck />}>
|
|
117
|
+
<Text size="sm">
|
|
118
|
+
Verification link sent successfully to {user.email}.
|
|
119
|
+
</Text>
|
|
120
|
+
</Alert>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<Group>
|
|
124
|
+
<ActionButton
|
|
125
|
+
leftSection={<IconMail size={16} />}
|
|
126
|
+
loading={verifyLoading}
|
|
127
|
+
onClick={handleTriggerEmailVerification}
|
|
128
|
+
>
|
|
129
|
+
Send Verification Link
|
|
130
|
+
</ActionButton>
|
|
131
|
+
</Group>
|
|
132
|
+
</Stack>
|
|
133
|
+
</Card>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
<Card withBorder p="lg">
|
|
137
|
+
<Stack gap="md">
|
|
138
|
+
<Text size="lg" fw={500} c="red">
|
|
139
|
+
Danger Zone
|
|
140
|
+
</Text>
|
|
141
|
+
|
|
142
|
+
<Alert variant="light" color="red" icon={<IconAlertCircle />}>
|
|
143
|
+
<Text size="sm">
|
|
144
|
+
Deleting this user will permanently remove their account and all
|
|
145
|
+
associated data. This action cannot be undone.
|
|
146
|
+
</Text>
|
|
147
|
+
</Alert>
|
|
148
|
+
|
|
149
|
+
<Group>
|
|
150
|
+
<ActionButton
|
|
151
|
+
color="red"
|
|
152
|
+
leftSection={<IconTrash size={16} />}
|
|
153
|
+
loading={deleteLoading}
|
|
154
|
+
onClick={handleDelete}
|
|
155
|
+
>
|
|
156
|
+
Delete User
|
|
157
|
+
</ActionButton>
|
|
158
|
+
</Group>
|
|
159
|
+
</Stack>
|
|
160
|
+
</Card>
|
|
161
|
+
</Flex>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export default AdminUserSettings;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { useClient } from "@alepha/react";
|
|
1
|
+
import { useClient, useRouter } from "@alepha/react";
|
|
2
2
|
import { useI18n } from "@alepha/react/i18n";
|
|
3
|
-
import {
|
|
4
|
-
import { Badge, Group } from "@mantine/core";
|
|
5
|
-
import { IconCheck, IconX } from "@tabler/icons-react";
|
|
3
|
+
import { ActionButton, DataTable, Text } from "@alepha/ui";
|
|
4
|
+
import { Badge, Flex, Group } from "@mantine/core";
|
|
5
|
+
import { IconCheck, IconPlus, IconX } from "@tabler/icons-react";
|
|
6
6
|
import { type Page, t } from "alepha";
|
|
7
7
|
import { type UserController, type UserEntity, users } from "alepha/api/users";
|
|
8
|
+
import type { AdminRouter } from "../AdminRouter.ts";
|
|
8
9
|
|
|
9
10
|
export interface AdminUsersProps {
|
|
10
11
|
userRealmName?: string;
|
|
@@ -12,6 +13,7 @@ export interface AdminUsersProps {
|
|
|
12
13
|
|
|
13
14
|
const AdminUsers = (props: AdminUsersProps) => {
|
|
14
15
|
const client = useClient<UserController>();
|
|
16
|
+
const router = useRouter<AdminRouter>();
|
|
15
17
|
const { l } = useI18n();
|
|
16
18
|
|
|
17
19
|
const filters = t.object({
|
|
@@ -25,7 +27,16 @@ const AdminUsers = (props: AdminUsersProps) => {
|
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
return (
|
|
28
|
-
<Flex flex={1}>
|
|
30
|
+
<Flex flex={1} direction="column">
|
|
31
|
+
<Flex justify="flex-end" p="md" pb={0}>
|
|
32
|
+
<ActionButton
|
|
33
|
+
leftSection={<IconPlus size={16} />}
|
|
34
|
+
href={router.path("adminUserCreate")}
|
|
35
|
+
>
|
|
36
|
+
Create User
|
|
37
|
+
</ActionButton>
|
|
38
|
+
</Flex>
|
|
39
|
+
|
|
29
40
|
<DataTable<UserEntity, typeof filters>
|
|
30
41
|
submitOnInit
|
|
31
42
|
defaultSize={10}
|
|
@@ -36,6 +47,8 @@ const AdminUsers = (props: AdminUsersProps) => {
|
|
|
36
47
|
tableProps={{
|
|
37
48
|
horizontalSpacing: "xs",
|
|
38
49
|
verticalSpacing: "xs",
|
|
50
|
+
striped: false,
|
|
51
|
+
highlightOnHover: true,
|
|
39
52
|
}}
|
|
40
53
|
onFilterChange={(key, value, form) => {
|
|
41
54
|
if (key === "query") {
|
|
@@ -44,12 +57,19 @@ const AdminUsers = (props: AdminUsersProps) => {
|
|
|
44
57
|
}}
|
|
45
58
|
filters={filters}
|
|
46
59
|
tableTrProps={(item) => {
|
|
60
|
+
const baseProps: Record<string, any> = {
|
|
61
|
+
style: { cursor: "pointer" },
|
|
62
|
+
onClick: () =>
|
|
63
|
+
router.go("adminUserDetails", {
|
|
64
|
+
params: { userId: item.id },
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
|
|
47
68
|
if (!item.enabled) {
|
|
48
|
-
|
|
49
|
-
opacity: 0.5,
|
|
50
|
-
};
|
|
69
|
+
baseProps.opacity = 0.5;
|
|
51
70
|
}
|
|
52
|
-
|
|
71
|
+
|
|
72
|
+
return baseProps;
|
|
53
73
|
}}
|
|
54
74
|
items={async (filters) => {
|
|
55
75
|
const response = await client.findUsers({
|
package/src/admin/index.ts
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { AlephaUI } from "@alepha/ui";
|
|
2
|
+
import { AlephaUIAuth } from "@alepha/ui/auth";
|
|
2
3
|
import { $module } from "alepha";
|
|
3
4
|
import { AdminRouter } from "./AdminRouter.ts";
|
|
5
|
+
import { AdminSidebar } from "./AdminSidebar.ts";
|
|
6
|
+
import { MainRouter } from "./MainRouter.ts";
|
|
4
7
|
|
|
5
8
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
6
9
|
|
|
7
10
|
export { AdminRouter } from "./AdminRouter.ts";
|
|
11
|
+
export { AdminSidebar } from "./AdminSidebar.ts";
|
|
8
12
|
export { default as AdminFiles } from "./components/AdminFiles.tsx";
|
|
9
13
|
export { default as AdminJobs } from "./components/AdminJobs.tsx";
|
|
10
14
|
export { default as AdminLayout } from "./components/AdminLayout.tsx";
|
|
11
15
|
export { default as AdminNotifications } from "./components/AdminNotifications.tsx";
|
|
12
16
|
export { default as AdminParameters } from "./components/AdminParameters.tsx";
|
|
13
|
-
export type { AdminSessionsProps } from "./components/AdminSessions.tsx";
|
|
14
17
|
export { default as AdminSessions } from "./components/AdminSessions.tsx";
|
|
15
|
-
export
|
|
18
|
+
export { default as AdminUserCreate } from "./components/AdminUserCreate.tsx";
|
|
19
|
+
export { default as AdminUserDetails } from "./components/AdminUserDetails.tsx";
|
|
20
|
+
export { default as AdminUserLayout } from "./components/AdminUserLayout.tsx";
|
|
21
|
+
export { default as AdminUserSessions } from "./components/AdminUserSessions.tsx";
|
|
22
|
+
export { default as AdminUserSettings } from "./components/AdminUserSettings.tsx";
|
|
16
23
|
export { default as AdminUsers } from "./components/AdminUsers.tsx";
|
|
17
24
|
export { default as AdminVerifications } from "./components/AdminVerifications.tsx";
|
|
25
|
+
export { MainRouter } from "./MainRouter.ts";
|
|
18
26
|
|
|
19
27
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
20
28
|
|
|
@@ -25,5 +33,9 @@ export { default as AdminVerifications } from "./components/AdminVerifications.t
|
|
|
25
33
|
*/
|
|
26
34
|
export const AlephaUIAdmin = $module({
|
|
27
35
|
name: "alepha.ui.admin",
|
|
28
|
-
services: [AlephaUI, AdminRouter],
|
|
36
|
+
services: [AlephaUI, AlephaUIAuth, AdminRouter, MainRouter, AdminSidebar],
|
|
37
|
+
register: (alepha) => {
|
|
38
|
+
alepha.with(AdminRouter);
|
|
39
|
+
alepha.with(AdminSidebar);
|
|
40
|
+
},
|
|
29
41
|
});
|
package/src/auth/AuthI18n.ts
CHANGED
|
@@ -54,6 +54,17 @@ export class AuthI18n {
|
|
|
54
54
|
resetPasswordCancel: "Cancel",
|
|
55
55
|
resetPasswordDisabled:
|
|
56
56
|
"Password reset is not available. Please contact your administrator.",
|
|
57
|
+
verifyEmailTitle: "Email Verification",
|
|
58
|
+
verifyEmailVerifying: "Verifying your email...",
|
|
59
|
+
verifyEmailPleaseWait:
|
|
60
|
+
"Please wait while we verify your email address.",
|
|
61
|
+
verifyEmailSuccess: "Your email has been verified successfully.",
|
|
62
|
+
verifyEmailFailed:
|
|
63
|
+
"Failed to verify your email. The link may have expired or is invalid.",
|
|
64
|
+
verifyEmailMissingParams:
|
|
65
|
+
"Invalid verification link. Email and token are required.",
|
|
66
|
+
verifyEmailSignIn: "Sign in to your account",
|
|
67
|
+
verifyEmailBackToSignIn: "Back to sign in",
|
|
57
68
|
},
|
|
58
69
|
}),
|
|
59
70
|
});
|
|
@@ -112,6 +123,17 @@ export class AuthI18n {
|
|
|
112
123
|
resetPasswordCancel: "Annuler",
|
|
113
124
|
resetPasswordDisabled:
|
|
114
125
|
"La réinitialisation du mot de passe n'est pas disponible. Veuillez contacter votre administrateur.",
|
|
126
|
+
verifyEmailTitle: "Vérification de l'e-mail",
|
|
127
|
+
verifyEmailVerifying: "Vérification de votre e-mail...",
|
|
128
|
+
verifyEmailPleaseWait:
|
|
129
|
+
"Veuillez patienter pendant que nous vérifions votre adresse e-mail.",
|
|
130
|
+
verifyEmailSuccess: "Votre e-mail a été vérifié avec succès.",
|
|
131
|
+
verifyEmailFailed:
|
|
132
|
+
"Échec de la vérification de votre e-mail. Le lien a peut-être expiré ou est invalide.",
|
|
133
|
+
verifyEmailMissingParams:
|
|
134
|
+
"Lien de vérification invalide. L'e-mail et le jeton sont requis.",
|
|
135
|
+
verifyEmailSignIn: "Se connecter à votre compte",
|
|
136
|
+
verifyEmailBackToSignIn: "Retour à la connexion",
|
|
115
137
|
},
|
|
116
138
|
}),
|
|
117
139
|
});
|
package/src/auth/AuthRouter.ts
CHANGED
|
@@ -1,53 +1,127 @@
|
|
|
1
1
|
import { $page } from "@alepha/react";
|
|
2
|
-
import {
|
|
2
|
+
import { ReactAuth } from "@alepha/react/auth";
|
|
3
|
+
import {
|
|
4
|
+
IconLogin2,
|
|
5
|
+
IconLogout2,
|
|
6
|
+
IconMailCheck,
|
|
7
|
+
IconPasswordUser,
|
|
8
|
+
IconUserPlus,
|
|
9
|
+
} from "@tabler/icons-react";
|
|
10
|
+
import { $inject, AlephaError, t } from "alepha";
|
|
3
11
|
import type { UserRealmController } from "alepha/api/users";
|
|
4
12
|
import { $client } from "alepha/server/links";
|
|
5
13
|
|
|
6
14
|
export class AuthRouter {
|
|
7
|
-
userRealmClient = $client<UserRealmController>();
|
|
15
|
+
protected readonly userRealmClient = $client<UserRealmController>();
|
|
16
|
+
protected readonly auth = $inject(ReactAuth);
|
|
17
|
+
|
|
18
|
+
layout = $page({
|
|
19
|
+
name: "AuthLayout",
|
|
20
|
+
path: "/auth",
|
|
21
|
+
lazy: () => import("./components/AuthLayout.tsx"),
|
|
22
|
+
children: () => [
|
|
23
|
+
this.login,
|
|
24
|
+
this.register,
|
|
25
|
+
this.resetPassword,
|
|
26
|
+
this.verifyEmail,
|
|
27
|
+
],
|
|
28
|
+
});
|
|
8
29
|
|
|
9
30
|
login = $page({
|
|
31
|
+
icon: IconLogin2,
|
|
32
|
+
label: "Sign In",
|
|
33
|
+
description: "Sign in to your account",
|
|
10
34
|
path: "/login",
|
|
11
35
|
schema: {
|
|
12
36
|
query: t.object({
|
|
13
|
-
|
|
37
|
+
r: t.optional(t.string()),
|
|
14
38
|
}),
|
|
15
39
|
},
|
|
40
|
+
can: () => !this.auth.user,
|
|
16
41
|
lazy: () => import("./components/Login.tsx"),
|
|
17
42
|
resolve: async () => {
|
|
18
43
|
return {
|
|
19
|
-
realmConfig: await this.
|
|
44
|
+
realmConfig: await this.loadRealmConfig(),
|
|
20
45
|
};
|
|
21
46
|
},
|
|
22
47
|
});
|
|
23
48
|
|
|
24
49
|
register = $page({
|
|
50
|
+
icon: IconUserPlus,
|
|
51
|
+
label: "Register",
|
|
52
|
+
description: "Create a new account",
|
|
25
53
|
path: "/register",
|
|
26
54
|
schema: {
|
|
27
55
|
query: t.object({
|
|
28
|
-
|
|
56
|
+
r: t.optional(t.string()),
|
|
29
57
|
}),
|
|
30
58
|
},
|
|
59
|
+
can: () => !this.auth.user,
|
|
31
60
|
lazy: () => import("./components/Register.tsx"),
|
|
32
61
|
resolve: async () => {
|
|
33
62
|
return {
|
|
34
|
-
realmConfig: await this.
|
|
63
|
+
realmConfig: await this.loadRealmConfig(),
|
|
35
64
|
};
|
|
36
65
|
},
|
|
37
66
|
});
|
|
38
67
|
|
|
39
68
|
resetPassword = $page({
|
|
69
|
+
icon: IconPasswordUser,
|
|
70
|
+
label: "Reset Password",
|
|
71
|
+
description: "Reset your account password",
|
|
40
72
|
path: "/reset-password",
|
|
41
73
|
schema: {
|
|
42
74
|
query: t.object({
|
|
43
|
-
|
|
75
|
+
r: t.optional(t.string()),
|
|
44
76
|
}),
|
|
45
77
|
},
|
|
78
|
+
can: () => !this.auth.user,
|
|
46
79
|
lazy: () => import("./components/ResetPassword.tsx"),
|
|
47
80
|
resolve: async () => {
|
|
48
81
|
return {
|
|
49
|
-
realmConfig: await this.
|
|
82
|
+
realmConfig: await this.loadRealmConfig(),
|
|
50
83
|
};
|
|
51
84
|
},
|
|
52
85
|
});
|
|
86
|
+
|
|
87
|
+
verifyEmail = $page({
|
|
88
|
+
icon: IconMailCheck,
|
|
89
|
+
label: "Verify Email",
|
|
90
|
+
description: "Verify your email address",
|
|
91
|
+
path: "/verify-email",
|
|
92
|
+
schema: {
|
|
93
|
+
query: t.object({
|
|
94
|
+
email: t.optional(t.string()),
|
|
95
|
+
token: t.optional(t.string()),
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
lazy: () => import("./components/VerifyEmail.tsx"),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
logout = $page({
|
|
102
|
+
icon: IconLogout2,
|
|
103
|
+
label: "Sign Out",
|
|
104
|
+
description: "Sign out of your account",
|
|
105
|
+
can: () => !!this.auth.user,
|
|
106
|
+
path: "/logout",
|
|
107
|
+
component: () => null,
|
|
108
|
+
resolve: () => {
|
|
109
|
+
this.auth.logout();
|
|
110
|
+
return {};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
protected async loadRealmConfig() {
|
|
115
|
+
try {
|
|
116
|
+
return await this.userRealmClient.getRealmConfig();
|
|
117
|
+
} catch (e) {
|
|
118
|
+
if (e instanceof AlephaError) {
|
|
119
|
+
throw new AlephaError(
|
|
120
|
+
"Missing User-Realm Configuration - Did you forget to add '$userRealm()' to your application?",
|
|
121
|
+
e,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
53
127
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NestedView } from "@alepha/react";
|
|
2
|
+
import { Flex } from "@mantine/core";
|
|
3
|
+
|
|
4
|
+
const AuthLayout = () => {
|
|
5
|
+
return (
|
|
6
|
+
<Flex flex={1} align={"center"} h={"100vh"} justify={"center"}>
|
|
7
|
+
<NestedView />
|
|
8
|
+
</Flex>
|
|
9
|
+
);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default AuthLayout;
|
|
@@ -22,7 +22,7 @@ const Login = (props: LoginProps) => {
|
|
|
22
22
|
const auth = useAuth();
|
|
23
23
|
const router = useRouter<AuthRouter>();
|
|
24
24
|
const { tr } = useI18n<AuthI18n, "en">();
|
|
25
|
-
const redirect = router.query.
|
|
25
|
+
const redirect = router.query.r || "/";
|
|
26
26
|
|
|
27
27
|
const hasUsernamePassword = props.realmConfig.authenticationMethods.find(
|
|
28
28
|
(it) => it.type === "CREDENTIALS",
|
|
@@ -116,7 +116,7 @@ const Login = (props: LoginProps) => {
|
|
|
116
116
|
autoComplete: "current-password",
|
|
117
117
|
}}
|
|
118
118
|
/>
|
|
119
|
-
<ActionButton variant={"filled"} form={form}>
|
|
119
|
+
<ActionButton color={"blue"} variant={"filled"} form={form}>
|
|
120
120
|
{tr("loginSignIn")}
|
|
121
121
|
</ActionButton>
|
|
122
122
|
</Stack>
|
|
@@ -161,18 +161,20 @@ const Login = (props: LoginProps) => {
|
|
|
161
161
|
),
|
|
162
162
|
)}
|
|
163
163
|
</Stack>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
164
|
+
{settings.registrationAllowed && (
|
|
165
|
+
<Text size="sm" ta="center">
|
|
166
|
+
{tr("loginNoAccount")}{" "}
|
|
167
|
+
<ActionButton
|
|
168
|
+
href={router.path("register")}
|
|
169
|
+
anchorProps={{ inherit: true }}
|
|
170
|
+
>
|
|
171
|
+
{tr("loginSignUp")}
|
|
172
|
+
</ActionButton>
|
|
173
|
+
</Text>
|
|
174
|
+
)}
|
|
173
175
|
</Stack>
|
|
174
176
|
</Card>
|
|
175
|
-
<ActionButton variant={"subtle"} href={
|
|
177
|
+
<ActionButton variant={"subtle"} href={"/"}>
|
|
176
178
|
{tr("loginCancel")}
|
|
177
179
|
</ActionButton>
|
|
178
180
|
</Stack>
|
|
@@ -43,7 +43,7 @@ const Register = (props: RegisterProps) => {
|
|
|
43
43
|
const userCtrl = useClient<UserController>();
|
|
44
44
|
const router = useRouter<AuthRouter>();
|
|
45
45
|
const { tr } = useI18n<AuthI18n, "en">();
|
|
46
|
-
const redirect = router.query.
|
|
46
|
+
const redirect = router.query.r || "/";
|
|
47
47
|
|
|
48
48
|
const [registrationState, setRegistrationState] = useState<RegistrationState>(
|
|
49
49
|
{
|
|
@@ -223,7 +223,7 @@ const Register = (props: RegisterProps) => {
|
|
|
223
223
|
{registrationState.intent.expectEmailVerification && (
|
|
224
224
|
<Stack gap={"xs"}>
|
|
225
225
|
<Text size="sm" fw={500}>
|
|
226
|
-
{tr("registerEmailCode")
|
|
226
|
+
{tr("registerEmailCode")}
|
|
227
227
|
</Text>
|
|
228
228
|
<Flex justify="center">
|
|
229
229
|
<PinInput
|
|
@@ -241,7 +241,7 @@ const Register = (props: RegisterProps) => {
|
|
|
241
241
|
{registrationState.intent.expectPhoneVerification && (
|
|
242
242
|
<Stack gap={"xs"}>
|
|
243
243
|
<Text size="sm" fw={500}>
|
|
244
|
-
{tr("registerPhoneCode")
|
|
244
|
+
{tr("registerPhoneCode")}
|
|
245
245
|
</Text>
|
|
246
246
|
<Flex justify="center">
|
|
247
247
|
<PinInput
|
|
@@ -257,11 +257,12 @@ const Register = (props: RegisterProps) => {
|
|
|
257
257
|
)}
|
|
258
258
|
|
|
259
259
|
<ActionButton
|
|
260
|
+
color={"blue"}
|
|
260
261
|
onClick={handleVerificationSubmit}
|
|
261
262
|
loading={isSubmitting}
|
|
262
263
|
disabled={!canSubmitVerification()}
|
|
263
264
|
>
|
|
264
|
-
{tr("registerVerifySubmit")
|
|
265
|
+
{tr("registerVerifySubmit")}
|
|
265
266
|
</ActionButton>
|
|
266
267
|
|
|
267
268
|
<ActionButton
|
|
@@ -350,7 +351,7 @@ const Register = (props: RegisterProps) => {
|
|
|
350
351
|
autoComplete: "new-password",
|
|
351
352
|
}}
|
|
352
353
|
/>
|
|
353
|
-
<ActionButton form={form}>
|
|
354
|
+
<ActionButton form={form} color={"blue"} variant={"filled"}>
|
|
354
355
|
{tr("registerCreateAccount")}
|
|
355
356
|
</ActionButton>
|
|
356
357
|
</Stack>
|
|
@@ -41,7 +41,7 @@ const ResetPassword = (props: ResetPasswordProps) => {
|
|
|
41
41
|
const [resetState, setResetState] = useState<ResetState>({ step: "email" });
|
|
42
42
|
const [error, setError] = useState<string | null>(null);
|
|
43
43
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
44
|
-
const redirect = router.query.
|
|
44
|
+
const redirect = router.query.r || "/";
|
|
45
45
|
|
|
46
46
|
const isResetPasswordAllowed =
|
|
47
47
|
props.realmConfig.settings?.resetPasswordAllowed !== false;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useClient, useRouter, useRouterState } from "@alepha/react";
|
|
2
|
+
import { useI18n } from "@alepha/react/i18n";
|
|
3
|
+
import { ActionButton } from "@alepha/ui";
|
|
4
|
+
import { Alert, Card, Flex, Loader, Stack, Text } from "@mantine/core";
|
|
5
|
+
import { IconAlertCircle, IconCheck, IconMailCheck } from "@tabler/icons-react";
|
|
6
|
+
import type { UserController } from "alepha/api/users";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import type { AuthI18n } from "../AuthI18n.ts";
|
|
9
|
+
import type { AuthRouter } from "../AuthRouter.ts";
|
|
10
|
+
|
|
11
|
+
export type VerifyEmailProps = {};
|
|
12
|
+
|
|
13
|
+
type Step = "verifying" | "success" | "error";
|
|
14
|
+
|
|
15
|
+
const VerifyEmail = (_props: VerifyEmailProps) => {
|
|
16
|
+
const router = useRouter<AuthRouter>();
|
|
17
|
+
const state = useRouterState();
|
|
18
|
+
const userCtrl = useClient<UserController>();
|
|
19
|
+
const { tr } = useI18n<AuthI18n, "en">();
|
|
20
|
+
|
|
21
|
+
const [step, setStep] = useState<Step>("verifying");
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const email = state.query.email as string | undefined;
|
|
25
|
+
const token = state.query.token as string | undefined;
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const verify = async () => {
|
|
29
|
+
if (!email || !token) {
|
|
30
|
+
setError(tr("verifyEmailMissingParams"));
|
|
31
|
+
setStep("error");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await userCtrl.verifyEmail({
|
|
37
|
+
body: { email, token },
|
|
38
|
+
});
|
|
39
|
+
setStep("success");
|
|
40
|
+
} catch (err) {
|
|
41
|
+
setError(err instanceof Error ? err.message : tr("verifyEmailFailed"));
|
|
42
|
+
setStep("error");
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
verify();
|
|
47
|
+
}, [email, token]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Flex flex={1} justify="center" align="center">
|
|
51
|
+
<Stack gap="sm" w={400}>
|
|
52
|
+
<Card withBorder p="lg" bg="var(--alepha-elevated)">
|
|
53
|
+
<Stack gap="md" align="center">
|
|
54
|
+
{step === "verifying" && (
|
|
55
|
+
<>
|
|
56
|
+
<Loader size="lg" />
|
|
57
|
+
<Text size="lg" fw={500} ta="center">
|
|
58
|
+
{tr("verifyEmailVerifying")}
|
|
59
|
+
</Text>
|
|
60
|
+
<Text size="sm" c="dimmed" ta="center">
|
|
61
|
+
{tr("verifyEmailPleaseWait")}
|
|
62
|
+
</Text>
|
|
63
|
+
</>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{step === "success" && (
|
|
67
|
+
<>
|
|
68
|
+
<IconMailCheck size={48} color="var(--mantine-color-green-6)" />
|
|
69
|
+
<Text size="lg" fw={500} ta="center">
|
|
70
|
+
{tr("verifyEmailTitle")}
|
|
71
|
+
</Text>
|
|
72
|
+
<Alert variant="light" color="green" icon={<IconCheck />}>
|
|
73
|
+
<Text size="sm">{tr("verifyEmailSuccess")}</Text>
|
|
74
|
+
</Alert>
|
|
75
|
+
<ActionButton href={router.path("login")} fullWidth>
|
|
76
|
+
{tr("verifyEmailSignIn")}
|
|
77
|
+
</ActionButton>
|
|
78
|
+
</>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{step === "error" && (
|
|
82
|
+
<>
|
|
83
|
+
<IconAlertCircle size={48} color="var(--mantine-color-red-6)" />
|
|
84
|
+
<Text size="lg" fw={500} ta="center">
|
|
85
|
+
{tr("verifyEmailTitle")}
|
|
86
|
+
</Text>
|
|
87
|
+
<Alert variant="light" color="red" icon={<IconAlertCircle />}>
|
|
88
|
+
<Text size="sm">{error || tr("verifyEmailFailed")}</Text>
|
|
89
|
+
</Alert>
|
|
90
|
+
<ActionButton href={router.path("login")} fullWidth>
|
|
91
|
+
{tr("verifyEmailBackToSignIn")}
|
|
92
|
+
</ActionButton>
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
</Stack>
|
|
96
|
+
</Card>
|
|
97
|
+
</Stack>
|
|
98
|
+
</Flex>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default VerifyEmail;
|