@alepha/ui 0.13.1 → 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.
Files changed (118) hide show
  1. package/dist/admin/AdminLayout-JakF7ESb.js +388 -0
  2. package/dist/admin/AdminLayout-JakF7ESb.js.map +1 -0
  3. package/dist/admin/AdminLayout-qNsIyl30.js +3 -0
  4. package/dist/admin/AdminNotifications-BPrxALdS.js +154 -0
  5. package/dist/admin/AdminNotifications-BPrxALdS.js.map +1 -0
  6. package/dist/admin/AdminNotifications-DV-35Fi3.js +3 -0
  7. package/dist/admin/{AdminSessions-CmDVneE2.js → AdminSessions-CMmBtbSw.js} +36 -9
  8. package/dist/admin/AdminSessions-CMmBtbSw.js.map +1 -0
  9. package/dist/admin/AdminSessions-Df2VYzlE.js +3 -0
  10. package/dist/admin/AdminUserCreate-Coa_yi6m.js +103 -0
  11. package/dist/admin/AdminUserCreate-Coa_yi6m.js.map +1 -0
  12. package/dist/admin/AdminUserCreate-DjiCcAk0.js +3 -0
  13. package/dist/admin/AdminUserDetails-BCFwOm9w.js +221 -0
  14. package/dist/admin/AdminUserDetails-BCFwOm9w.js.map +1 -0
  15. package/dist/admin/AdminUserDetails-C5yeJNa3.js +3 -0
  16. package/dist/admin/AdminUserLayout-B8ga5QvP.js +3 -0
  17. package/dist/admin/AdminUserLayout-CR2OqV9Z.js +153 -0
  18. package/dist/admin/AdminUserLayout-CR2OqV9Z.js.map +1 -0
  19. package/dist/admin/AdminUserSessions-A_5KkqTY.js +3 -0
  20. package/dist/admin/AdminUserSessions-Bcf6-rjG.js +129 -0
  21. package/dist/admin/AdminUserSessions-Bcf6-rjG.js.map +1 -0
  22. package/dist/admin/AdminUserSettings-DAsAhFjX.js +3 -0
  23. package/dist/admin/AdminUserSettings-DRYVdW6S.js +164 -0
  24. package/dist/admin/AdminUserSettings-DRYVdW6S.js.map +1 -0
  25. package/dist/admin/AdminUsers-Dd9a5UqO.js +3 -0
  26. package/dist/admin/{AdminUsers-88De5pev.js → AdminUsers-IN_2yHKt.js} +32 -14
  27. package/dist/admin/AdminUsers-IN_2yHKt.js.map +1 -0
  28. package/dist/admin/index.d.ts +6052 -908
  29. package/dist/admin/index.js +299 -41
  30. package/dist/admin/index.js.map +1 -1
  31. package/dist/auth/AuthLayout-BSL8ZHgr.js +19 -0
  32. package/dist/auth/AuthLayout-BSL8ZHgr.js.map +1 -0
  33. package/dist/auth/Login-DDsyCNAA.js +4 -0
  34. package/dist/auth/{Login-OCrvjs9U.js → Login-kBfaRgKG.js} +5 -4
  35. package/dist/auth/Login-kBfaRgKG.js.map +1 -0
  36. package/dist/auth/{Register-Ei34GSba.js → Register-BxJmOqpF.js} +9 -6
  37. package/dist/auth/Register-BxJmOqpF.js.map +1 -0
  38. package/dist/auth/Register-D10MnlQc.js +4 -0
  39. package/dist/auth/{ResetPassword-tO0oMzfo.js → ResetPassword-BhyZ9ek4.js} +3 -3
  40. package/dist/auth/ResetPassword-BhyZ9ek4.js.map +1 -0
  41. package/dist/auth/ResetPassword-llBG-STp.js +3 -0
  42. package/dist/auth/VerifyEmail-BvOG-IUC.js +3 -0
  43. package/dist/auth/VerifyEmail-DeLct3oQ.js +131 -0
  44. package/dist/auth/VerifyEmail-DeLct3oQ.js.map +1 -0
  45. package/dist/auth/index.d.ts +3773 -3568
  46. package/dist/auth/index.js +96 -20
  47. package/dist/auth/index.js.map +1 -1
  48. package/dist/core/index.d.ts +340 -155
  49. package/dist/core/index.js +1391 -395
  50. package/dist/core/index.js.map +1 -1
  51. package/package.json +11 -8
  52. package/src/admin/AdminRouter.ts +116 -29
  53. package/src/admin/MainRouter.ts +23 -0
  54. package/src/admin/components/AdminLayout.tsx +86 -103
  55. package/src/admin/components/AdminNotifications.tsx +196 -12
  56. package/src/admin/components/AdminParameters.tsx +1 -1
  57. package/src/admin/components/AdminSessions.tsx +43 -7
  58. package/src/admin/components/AdminUserCreate.tsx +84 -0
  59. package/src/admin/components/AdminUserDetails.tsx +180 -0
  60. package/src/admin/components/AdminUserLayout.tsx +172 -0
  61. package/src/admin/components/AdminUserSessions.tsx +158 -0
  62. package/src/admin/components/AdminUserSettings.tsx +165 -0
  63. package/src/admin/components/AdminUsers.tsx +29 -9
  64. package/src/admin/index.ts +12 -3
  65. package/src/auth/AuthI18n.ts +22 -0
  66. package/src/auth/AuthRouter.ts +82 -8
  67. package/src/auth/components/AuthLayout.tsx +12 -0
  68. package/src/auth/components/Login.tsx +13 -11
  69. package/src/auth/components/Register.tsx +6 -5
  70. package/src/auth/components/ResetPassword.tsx +1 -1
  71. package/src/auth/components/VerifyEmail.tsx +102 -0
  72. package/src/auth/components/buttons/UserButton.tsx +6 -2
  73. package/src/auth/index.ts +1 -0
  74. package/src/core/components/buttons/ActionButton.tsx +11 -4
  75. package/src/core/components/buttons/DarkModeButton.tsx +1 -1
  76. package/src/core/components/buttons/OmnibarButton.tsx +10 -5
  77. package/src/core/components/buttons/ThemeButton.tsx +31 -0
  78. package/src/core/components/layout/AdminShell.tsx +4 -2
  79. package/src/core/components/layout/AlephaMantineProvider.tsx +10 -4
  80. package/src/core/components/layout/Omnibar.tsx +27 -10
  81. package/src/core/components/layout/Sidebar.tsx +33 -15
  82. package/src/core/components/table/DataTable.tsx +9 -5
  83. package/src/core/hooks/useTheme.ts +25 -0
  84. package/src/core/index.ts +9 -4
  85. package/src/core/providers/ThemeProvider.ts +87 -0
  86. package/src/core/themes/aurora.ts +107 -0
  87. package/src/core/themes/crystal.ts +107 -0
  88. package/src/core/themes/default.ts +7 -0
  89. package/src/core/themes/ember.ts +107 -0
  90. package/src/core/themes/index.ts +7 -0
  91. package/src/core/themes/midnight.ts +104 -0
  92. package/src/core/themes/remoraid.ts +278 -0
  93. package/src/core/themes/slate.ts +81 -0
  94. package/dist/admin/AdminJobs-BOq6AZOW.js +0 -3
  95. package/dist/admin/AdminJobs-CDnVxEv6.js +0 -125
  96. package/dist/admin/AdminJobs-CDnVxEv6.js.map +0 -1
  97. package/dist/admin/AdminLayout-Bgx25J8m.js +0 -3
  98. package/dist/admin/AdminLayout-CervL8LV.js +0 -88
  99. package/dist/admin/AdminLayout-CervL8LV.js.map +0 -1
  100. package/dist/admin/AdminNotifications-BDQXt3-e.js +0 -3
  101. package/dist/admin/AdminNotifications-DvI2989x.js +0 -40
  102. package/dist/admin/AdminNotifications-DvI2989x.js.map +0 -1
  103. package/dist/admin/AdminParameters-CWi7crdn.js +0 -40
  104. package/dist/admin/AdminParameters-CWi7crdn.js.map +0 -1
  105. package/dist/admin/AdminParameters-DKRAVen3.js +0 -3
  106. package/dist/admin/AdminSessions-CmDVneE2.js.map +0 -1
  107. package/dist/admin/AdminSessions-Dkk_fzWK.js +0 -3
  108. package/dist/admin/AdminUsers-88De5pev.js.map +0 -1
  109. package/dist/admin/AdminUsers-oyAXqZ5l.js +0 -3
  110. package/dist/admin/AdminVerifications-D93TKymL.js +0 -3
  111. package/dist/admin/AdminVerifications-DBVEoqJe.js +0 -40
  112. package/dist/admin/AdminVerifications-DBVEoqJe.js.map +0 -1
  113. package/dist/auth/Login-BC2jTczq.js +0 -4
  114. package/dist/auth/Login-OCrvjs9U.js.map +0 -1
  115. package/dist/auth/Register-Dh0lsQmI.js +0 -4
  116. package/dist/auth/Register-Ei34GSba.js.map +0 -1
  117. package/dist/auth/ResetPassword-BnlAQAOE.js +0 -3
  118. 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 { DataTable, Flex, Text } from "@alepha/ui";
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
- return {
49
- opacity: 0.5,
50
- };
69
+ baseProps.opacity = 0.5;
51
70
  }
52
- return {};
71
+
72
+ return baseProps;
53
73
  }}
54
74
  items={async (filters) => {
55
75
  const response = await client.findUsers({
@@ -1,6 +1,8 @@
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 { MainRouter } from "./MainRouter.ts";
4
6
 
5
7
  // ---------------------------------------------------------------------------------------------------------------------
6
8
 
@@ -10,11 +12,15 @@ export { default as AdminJobs } from "./components/AdminJobs.tsx";
10
12
  export { default as AdminLayout } from "./components/AdminLayout.tsx";
11
13
  export { default as AdminNotifications } from "./components/AdminNotifications.tsx";
12
14
  export { default as AdminParameters } from "./components/AdminParameters.tsx";
13
- export type { AdminSessionsProps } from "./components/AdminSessions.tsx";
14
15
  export { default as AdminSessions } from "./components/AdminSessions.tsx";
15
- export type { AdminUsersProps } from "./components/AdminUsers.tsx";
16
+ export { default as AdminUserCreate } from "./components/AdminUserCreate.tsx";
17
+ export { default as AdminUserDetails } from "./components/AdminUserDetails.tsx";
18
+ export { default as AdminUserLayout } from "./components/AdminUserLayout.tsx";
19
+ export { default as AdminUserSessions } from "./components/AdminUserSessions.tsx";
20
+ export { default as AdminUserSettings } from "./components/AdminUserSettings.tsx";
16
21
  export { default as AdminUsers } from "./components/AdminUsers.tsx";
17
22
  export { default as AdminVerifications } from "./components/AdminVerifications.tsx";
23
+ export { MainRouter } from "./MainRouter.ts";
18
24
 
19
25
  // ---------------------------------------------------------------------------------------------------------------------
20
26
 
@@ -25,5 +31,8 @@ export { default as AdminVerifications } from "./components/AdminVerifications.t
25
31
  */
26
32
  export const AlephaUIAdmin = $module({
27
33
  name: "alepha.ui.admin",
28
- services: [AlephaUI, AdminRouter],
34
+ services: [AlephaUI, AlephaUIAuth, AdminRouter, MainRouter],
35
+ register: (alepha) => {
36
+ alepha.with(AdminRouter);
37
+ },
29
38
  });
@@ -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
  });
@@ -1,53 +1,127 @@
1
1
  import { $page } from "@alepha/react";
2
- import { t } from "alepha";
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
- redirect: t.optional(t.string()),
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.userRealmClient.getRealmConfig(),
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
- redirect: t.optional(t.string()),
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.userRealmClient.getRealmConfig(),
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
- redirect: t.optional(t.string()),
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.userRealmClient.getRealmConfig(),
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.redirect || "/";
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,15 +161,17 @@ const Login = (props: LoginProps) => {
161
161
  ),
162
162
  )}
163
163
  </Stack>
164
- <Text size="sm" ta="center">
165
- {tr("loginNoAccount")}{" "}
166
- <ActionButton
167
- href={router.path("register")}
168
- anchorProps={{ inherit: true }}
169
- >
170
- {tr("loginSignUp")}
171
- </ActionButton>
172
- </Text>
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
177
  <ActionButton variant={"subtle"} href={redirect}>
@@ -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.redirect || "/";
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") ?? "Email verification code"}
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") ?? "Phone verification code"}
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") ?? "Complete Registration"}
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.redirect || "/";
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;