@alepha/ui 0.12.0 → 0.12.1

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 (164) hide show
  1. package/README.md +2 -30
  2. package/dist/admin/AdminFiles-BM6_7_5A.cjs +4 -0
  3. package/dist/admin/AdminFiles-BaCIMeNt.js +4 -0
  4. package/dist/admin/AdminFiles-CllAxb1B.js +117 -0
  5. package/dist/admin/AdminFiles-CllAxb1B.js.map +1 -0
  6. package/dist/admin/AdminFiles-DC3T8uWZ.cjs +122 -0
  7. package/dist/admin/AdminFiles-DC3T8uWZ.cjs.map +1 -0
  8. package/dist/admin/AdminJobs-BXkFtlVo.js +125 -0
  9. package/dist/admin/AdminJobs-BXkFtlVo.js.map +1 -0
  10. package/dist/admin/AdminJobs-C428qrNQ.cjs +130 -0
  11. package/dist/admin/AdminJobs-C428qrNQ.cjs.map +1 -0
  12. package/dist/admin/AdminJobs-DCPPaJ4i.cjs +4 -0
  13. package/dist/admin/AdminJobs-yC6DarGO.js +4 -0
  14. package/dist/admin/AdminLayout-Bqo4cd33.cjs +4 -0
  15. package/dist/admin/AdminLayout-CQpxfko6.js +4 -0
  16. package/dist/admin/AdminLayout-CiLlywAQ.cjs +93 -0
  17. package/dist/admin/AdminLayout-CiLlywAQ.cjs.map +1 -0
  18. package/dist/admin/AdminLayout-CtkVYk-u.js +88 -0
  19. package/dist/admin/AdminLayout-CtkVYk-u.js.map +1 -0
  20. package/dist/admin/AdminNotifications-DNUeJ-PW.cjs +44 -0
  21. package/dist/admin/AdminNotifications-DNUeJ-PW.cjs.map +1 -0
  22. package/dist/admin/AdminNotifications-DaMu1AQ4.js +4 -0
  23. package/dist/admin/AdminNotifications-DnnulNNV.js +40 -0
  24. package/dist/admin/AdminNotifications-DnnulNNV.js.map +1 -0
  25. package/dist/admin/AdminNotifications-ihgbKVCx.cjs +4 -0
  26. package/dist/admin/AdminParameters-B3hvpLpu.js +40 -0
  27. package/dist/admin/AdminParameters-B3hvpLpu.js.map +1 -0
  28. package/dist/admin/AdminParameters-U4lU1rUF.cjs +4 -0
  29. package/dist/admin/AdminParameters-gdf7036N.cjs +44 -0
  30. package/dist/admin/AdminParameters-gdf7036N.cjs.map +1 -0
  31. package/dist/admin/AdminParameters-prMcCgxf.js +4 -0
  32. package/dist/admin/AdminSessions-BF_P4lHs.cjs +128 -0
  33. package/dist/admin/AdminSessions-BF_P4lHs.cjs.map +1 -0
  34. package/dist/admin/AdminSessions-CATIU61I.cjs +4 -0
  35. package/dist/admin/AdminSessions-DqOXOpYR.js +4 -0
  36. package/dist/admin/AdminSessions-Pjdz-iZx.js +123 -0
  37. package/dist/admin/AdminSessions-Pjdz-iZx.js.map +1 -0
  38. package/dist/admin/AdminUsers-BgTL-zSY.js +4 -0
  39. package/dist/admin/AdminUsers-C1HsrRxn.js +104 -0
  40. package/dist/admin/AdminUsers-C1HsrRxn.js.map +1 -0
  41. package/dist/admin/AdminUsers-HqvxwNGZ.cjs +4 -0
  42. package/dist/admin/AdminUsers-M2uEQbp5.cjs +109 -0
  43. package/dist/admin/AdminUsers-M2uEQbp5.cjs.map +1 -0
  44. package/dist/admin/AdminVerifications-BVssbtfU.cjs +44 -0
  45. package/dist/admin/AdminVerifications-BVssbtfU.cjs.map +1 -0
  46. package/dist/admin/AdminVerifications-Df6DRgNo.js +4 -0
  47. package/dist/admin/AdminVerifications-DxAtcYUR.cjs +4 -0
  48. package/dist/admin/AdminVerifications-VMpm30mS.js +40 -0
  49. package/dist/admin/AdminVerifications-VMpm30mS.js.map +1 -0
  50. package/dist/admin/core-CzO6aavT.js +2507 -0
  51. package/dist/admin/core-CzO6aavT.js.map +1 -0
  52. package/dist/{index.cjs → admin/core-aFtK4l9I.cjs} +287 -204
  53. package/dist/admin/core-aFtK4l9I.cjs.map +1 -0
  54. package/dist/admin/index.cjs +87 -0
  55. package/dist/admin/index.cjs.map +1 -0
  56. package/dist/admin/index.d.cts +1739 -0
  57. package/dist/admin/index.d.ts +1745 -0
  58. package/dist/admin/index.js +78 -0
  59. package/dist/admin/index.js.map +1 -0
  60. package/dist/auth/IconGoogle-B17BTQyD.cjs +69 -0
  61. package/dist/auth/IconGoogle-B17BTQyD.cjs.map +1 -0
  62. package/dist/auth/IconGoogle-Bfmuv9Rv.js +58 -0
  63. package/dist/auth/IconGoogle-Bfmuv9Rv.js.map +1 -0
  64. package/dist/auth/Login-BTBmbnWl.cjs +181 -0
  65. package/dist/auth/Login-BTBmbnWl.cjs.map +1 -0
  66. package/dist/auth/Login-BcQOtG3v.js +5 -0
  67. package/dist/auth/Login-Btmd70Um.cjs +5 -0
  68. package/dist/auth/Login-JeXFsUf5.js +176 -0
  69. package/dist/auth/Login-JeXFsUf5.js.map +1 -0
  70. package/dist/auth/Register-CPQnvXCZ.js +318 -0
  71. package/dist/auth/Register-CPQnvXCZ.js.map +1 -0
  72. package/dist/auth/Register-CbesZal3.cjs +5 -0
  73. package/dist/auth/Register-DpI_JdyO.js +5 -0
  74. package/dist/auth/Register-HP3rP71B.cjs +323 -0
  75. package/dist/auth/Register-HP3rP71B.cjs.map +1 -0
  76. package/dist/auth/ResetPassword-B-tkzV7g.cjs +248 -0
  77. package/dist/auth/ResetPassword-B-tkzV7g.cjs.map +1 -0
  78. package/dist/auth/ResetPassword-BlK3xEpU.js +4 -0
  79. package/dist/auth/ResetPassword-BzUjGG_-.js +243 -0
  80. package/dist/auth/ResetPassword-BzUjGG_-.js.map +1 -0
  81. package/dist/auth/ResetPassword-W3xjOnWy.cjs +4 -0
  82. package/dist/auth/chunk-DhGyd7sr.js +28 -0
  83. package/dist/auth/core-D1MHij1j.js +1795 -0
  84. package/dist/auth/core-D1MHij1j.js.map +1 -0
  85. package/dist/auth/core-rDZ9d92K.cjs +1824 -0
  86. package/dist/auth/core-rDZ9d92K.cjs.map +1 -0
  87. package/dist/auth/index.cjs +211 -0
  88. package/dist/auth/index.cjs.map +1 -0
  89. package/dist/auth/index.d.cts +6265 -0
  90. package/dist/auth/index.d.ts +6274 -0
  91. package/dist/auth/index.js +206 -0
  92. package/dist/auth/index.js.map +1 -0
  93. package/dist/core/index.cjs +2620 -0
  94. package/dist/core/index.cjs.map +1 -0
  95. package/dist/core/index.d.cts +2737 -0
  96. package/dist/core/index.d.ts +2743 -0
  97. package/dist/{index.js → core/index.js} +298 -126
  98. package/dist/core/index.js.map +1 -0
  99. package/package.json +32 -14
  100. package/src/admin/AdminRouter.ts +58 -0
  101. package/src/admin/components/AdminFiles.tsx +117 -0
  102. package/src/admin/components/AdminJobs.tsx +158 -0
  103. package/src/admin/components/AdminLayout.tsx +114 -0
  104. package/src/admin/components/AdminNotifications.tsx +20 -0
  105. package/src/admin/components/AdminParameters.tsx +24 -0
  106. package/src/admin/components/AdminSessions.tsx +159 -0
  107. package/src/admin/components/AdminUsers.tsx +137 -0
  108. package/src/admin/components/AdminVerifications.tsx +25 -0
  109. package/src/admin/index.ts +29 -0
  110. package/src/auth/AuthI18n.ts +118 -0
  111. package/src/auth/AuthRouter.ts +53 -0
  112. package/src/auth/components/Login.tsx +193 -0
  113. package/src/auth/components/Register.tsx +421 -0
  114. package/src/auth/components/ResetPassword.tsx +259 -0
  115. package/src/auth/components/buttons/UserButton.tsx +118 -0
  116. package/src/auth/components/icons/IconGithub.tsx +21 -0
  117. package/src/auth/components/icons/IconGoogle.tsx +30 -0
  118. package/src/auth/index.ts +27 -0
  119. package/src/{RootRouter.ts → core/RootRouter.ts} +2 -1
  120. package/src/{components → core/components}/buttons/ActionButton.tsx +49 -6
  121. package/src/core/components/buttons/ClipboardButton.tsx +56 -0
  122. package/src/{components → core/components}/buttons/DarkModeButton.tsx +7 -8
  123. package/src/{components → core/components}/buttons/LanguageButton.tsx +2 -2
  124. package/src/{components → core/components}/buttons/OmnibarButton.tsx +1 -1
  125. package/src/{components → core/components}/dialogs/AlertDialog.tsx +1 -1
  126. package/src/{components → core/components}/dialogs/ConfirmDialog.tsx +1 -1
  127. package/src/{components → core/components}/dialogs/PromptDialog.tsx +1 -1
  128. package/src/{components → core/components}/form/Control.tsx +1 -0
  129. package/src/{components → core/components}/layout/AdminShell.tsx +38 -7
  130. package/src/{components → core/components}/layout/AlephaMantineProvider.tsx +12 -8
  131. package/src/{components → core/components}/layout/AppBar.tsx +1 -1
  132. package/src/{components → core/components}/layout/Omnibar.tsx +1 -1
  133. package/src/{components → core/components}/layout/Sidebar.tsx +29 -26
  134. package/src/{components → core/components}/table/DataTable.tsx +1 -1
  135. package/src/{constants → core/constants}/ui.ts +9 -0
  136. package/src/{index.ts → core/index.ts} +3 -0
  137. package/src/{services → core/services}/DialogService.tsx +3 -3
  138. package/src/{services → core/services}/ToastService.tsx +3 -1
  139. package/src/{utils → core/utils}/extractSchemaFields.ts +2 -8
  140. package/src/{utils → core/utils}/icons.tsx +5 -15
  141. package/src/{utils → core/utils}/parseInput.ts +34 -26
  142. package/dist/AlephaMantineProvider-CGpgWDt8.cjs +0 -3
  143. package/dist/AlephaMantineProvider-D8cHYAge.js +0 -152
  144. package/dist/AlephaMantineProvider-D8cHYAge.js.map +0 -1
  145. package/dist/AlephaMantineProvider-DuvZFAuk.cjs +0 -175
  146. package/dist/AlephaMantineProvider-DuvZFAuk.cjs.map +0 -1
  147. package/dist/AlephaMantineProvider-twBqV4IO.js +0 -3
  148. package/dist/index.cjs.map +0 -1
  149. package/dist/index.d.cts +0 -821
  150. package/dist/index.d.cts.map +0 -1
  151. package/dist/index.d.ts +0 -821
  152. package/dist/index.d.ts.map +0 -1
  153. package/dist/index.js.map +0 -1
  154. /package/src/{components → core/components}/buttons/BurgerButton.tsx +0 -0
  155. /package/src/{components → core/components}/buttons/ToggleSidebarButton.tsx +0 -0
  156. /package/src/{components → core/components}/data/JsonViewer.tsx +0 -0
  157. /package/src/{components → core/components}/form/ControlDate.tsx +0 -0
  158. /package/src/{components → core/components}/form/ControlNumber.tsx +0 -0
  159. /package/src/{components → core/components}/form/ControlQueryBuilder.tsx +0 -0
  160. /package/src/{components → core/components}/form/ControlSelect.tsx +0 -0
  161. /package/src/{components → core/components}/form/TypeForm.tsx +0 -0
  162. /package/src/{hooks → core/hooks}/useDialog.ts +0 -0
  163. /package/src/{hooks → core/hooks}/useToast.ts +0 -0
  164. /package/src/{utils → core/utils}/string.ts +0 -0
@@ -0,0 +1,137 @@
1
+ import { useClient } from "@alepha/react";
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";
6
+ import { type Page, t } from "alepha";
7
+ import { type UserController, type UserEntity, users } from "alepha/api/users";
8
+
9
+ export interface AdminUsersProps {
10
+ userRealmName?: string;
11
+ }
12
+
13
+ const AdminUsers = (props: AdminUsersProps) => {
14
+ const client = useClient<UserController>();
15
+ const { l } = useI18n();
16
+
17
+ const filters = t.object({
18
+ query: t.optional(
19
+ t.string({
20
+ $control: {
21
+ query: t.omit(users.schema, ["id", "version"]),
22
+ },
23
+ }),
24
+ ),
25
+ });
26
+
27
+ return (
28
+ <Flex flex={1}>
29
+ <DataTable<UserEntity, typeof filters>
30
+ submitOnInit
31
+ defaultSize={10}
32
+ typeFormProps={{
33
+ skipSubmitButton: true,
34
+ columns: 3,
35
+ }}
36
+ tableProps={{
37
+ horizontalSpacing: "xs",
38
+ verticalSpacing: "xs",
39
+ }}
40
+ onFilterChange={(key, value, form) => {
41
+ if (key === "query") {
42
+ return form.submit();
43
+ }
44
+ }}
45
+ filters={filters}
46
+ tableTrProps={(item) => {
47
+ if (!item.enabled) {
48
+ return {
49
+ opacity: 0.5,
50
+ };
51
+ }
52
+ return {};
53
+ }}
54
+ items={async (filters) => {
55
+ const response = await client.findUsers({
56
+ query: {
57
+ ...filters,
58
+ userRealmName: props.userRealmName,
59
+ },
60
+ });
61
+
62
+ return response as Page<UserEntity>;
63
+ }}
64
+ columns={{
65
+ username: {
66
+ label: "Username",
67
+ value: (item) => (
68
+ <Text size="sm" fw={500}>
69
+ {item.username || "-"}
70
+ </Text>
71
+ ),
72
+ },
73
+ email: {
74
+ label: "Email",
75
+ value: (item) => (
76
+ <Group gap="xs">
77
+ <Text size="sm">{item.email || "-"}</Text>
78
+ {item.email && (
79
+ <Badge
80
+ size="xs"
81
+ variant="light"
82
+ color={item.emailVerified ? "green" : "gray"}
83
+ leftSection={
84
+ item.emailVerified ? (
85
+ <IconCheck size={10} />
86
+ ) : (
87
+ <IconX size={10} />
88
+ )
89
+ }
90
+ >
91
+ {item.emailVerified ? "Verified" : "Unverified"}
92
+ </Badge>
93
+ )}
94
+ </Group>
95
+ ),
96
+ },
97
+ roles: {
98
+ label: "Roles",
99
+ value: (item) => (
100
+ <Group gap={4}>
101
+ {item.roles.map((role: string) => (
102
+ <Badge key={role} size="xs" variant="outline">
103
+ {role}
104
+ </Badge>
105
+ ))}
106
+ </Group>
107
+ ),
108
+ },
109
+ enabled: {
110
+ label: "Status",
111
+ fit: true,
112
+ value: (item) => (
113
+ <Badge
114
+ size="sm"
115
+ variant="light"
116
+ color={item.enabled ? "green" : "red"}
117
+ >
118
+ {item.enabled ? "Active" : "Disabled"}
119
+ </Badge>
120
+ ),
121
+ },
122
+ createdAt: {
123
+ label: "Created",
124
+ fit: true,
125
+ value: (item) => (
126
+ <Text size="xs" c="dimmed">
127
+ {l(item.createdAt, { date: "fromNow" })}
128
+ </Text>
129
+ ),
130
+ },
131
+ }}
132
+ />
133
+ </Flex>
134
+ );
135
+ };
136
+
137
+ export default AdminUsers;
@@ -0,0 +1,25 @@
1
+ import { Flex, Text } from "@alepha/ui";
2
+ import { Stack } from "@mantine/core";
3
+ import { IconShieldCheck } from "@tabler/icons-react";
4
+
5
+ const AdminVerifications = () => {
6
+ return (
7
+ <Flex flex={1} justify="center" align="center">
8
+ <Stack align="center" gap="xs">
9
+ <IconShieldCheck
10
+ size={48}
11
+ stroke={1.5}
12
+ color="var(--mantine-color-dimmed)"
13
+ />
14
+ <Text c="dimmed">Verification Management</Text>
15
+ <Text size="xs" c="dimmed" ta="center" maw={400}>
16
+ Verifications are automatically managed by the system. Email and SMS
17
+ verification codes are generated and validated through the
18
+ verification API endpoints.
19
+ </Text>
20
+ </Stack>
21
+ </Flex>
22
+ );
23
+ };
24
+
25
+ export default AdminVerifications;
@@ -0,0 +1,29 @@
1
+ import { AlephaUI } from "@alepha/ui";
2
+ import { $module } from "alepha";
3
+ import { AdminRouter } from "./AdminRouter.ts";
4
+
5
+ // ---------------------------------------------------------------------------------------------------------------------
6
+
7
+ export { AdminRouter } from "./AdminRouter.ts";
8
+ export { default as AdminFiles } from "./components/AdminFiles.tsx";
9
+ export { default as AdminJobs } from "./components/AdminJobs.tsx";
10
+ export { default as AdminLayout } from "./components/AdminLayout.tsx";
11
+ export { default as AdminNotifications } from "./components/AdminNotifications.tsx";
12
+ export { default as AdminParameters } from "./components/AdminParameters.tsx";
13
+ export type { AdminSessionsProps } from "./components/AdminSessions.tsx";
14
+ export { default as AdminSessions } from "./components/AdminSessions.tsx";
15
+ export type { AdminUsersProps } from "./components/AdminUsers.tsx";
16
+ export { default as AdminUsers } from "./components/AdminUsers.tsx";
17
+ export { default as AdminVerifications } from "./components/AdminVerifications.tsx";
18
+
19
+ // ---------------------------------------------------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Admin panel UI Module
23
+ *
24
+ * @module alepha.ui.admin
25
+ */
26
+ export const AlephaUIAdmin = $module({
27
+ name: "alepha.ui.admin",
28
+ services: [AlephaUI, AdminRouter],
29
+ });
@@ -0,0 +1,118 @@
1
+ import { $dictionary } from "@alepha/react/i18n";
2
+
3
+ export class AuthI18n {
4
+ en = $dictionary({
5
+ name: "alepha.ui.auth.en",
6
+ lazy: () => ({
7
+ default: {
8
+ loginSignIn: "Sign in",
9
+ loginContinueWith: "Continue with $1",
10
+ loginOr: "OR",
11
+ loginCancel: "Cancel",
12
+ loginForgotPassword: "Forgot password?",
13
+ loginNoAccount: "Don't have an account?",
14
+ loginSignUp: "Sign up",
15
+ loginUsername: "Username",
16
+ loginEmail: "Email",
17
+ loginPhone: "Phone number",
18
+ loginPassword: "Password",
19
+ registerCreateAccount: "Create account",
20
+ registerContinueWith: "Continue with $1",
21
+ registerOr: "OR",
22
+ registerCancel: "Cancel",
23
+ registerHaveAccount: "Already have an account?",
24
+ registerSignIn: "Sign in",
25
+ registerUsername: "Username",
26
+ registerEmail: "Email",
27
+ registerPhone: "Phone number",
28
+ registerPassword: "Password",
29
+ registerConfirmPassword: "Confirm password",
30
+ registerDisabled:
31
+ "Registration is not available. Please contact your administrator.",
32
+ registerBackToSignIn: "Back to sign in",
33
+ registerVerifyTitle: "Verify your account",
34
+ registerVerifyDescription:
35
+ "Please enter the verification code(s) sent to you.",
36
+ registerEmailCode: "Email verification code",
37
+ registerPhoneCode: "Phone verification code",
38
+ registerVerifySubmit: "Complete Registration",
39
+ registerVerifyBack: "Back to registration",
40
+ resetPasswordTitle: "Reset password",
41
+ resetPasswordEmail: "Email",
42
+ resetPasswordEnterEmail:
43
+ "Enter your email address to reset your password",
44
+ resetPasswordSendCode: "Send verification code",
45
+ resetPasswordCodeSent: "We've sent a verification code to your email.",
46
+ resetPasswordEnterCode: "Enter the 6-digit code",
47
+ resetPasswordResendCode: "Resend code",
48
+ resetPasswordEnterNewPassword: "Create your new password",
49
+ resetPasswordNewPassword: "New password",
50
+ resetPasswordConfirmPassword: "Confirm password",
51
+ resetPasswordSetNewPassword: "Set new password",
52
+ resetPasswordSuccess: "Your password has been reset successfully.",
53
+ resetPasswordBackToSignIn: "Back to sign in",
54
+ resetPasswordCancel: "Cancel",
55
+ resetPasswordDisabled:
56
+ "Password reset is not available. Please contact your administrator.",
57
+ },
58
+ }),
59
+ });
60
+
61
+ fr = $dictionary({
62
+ lazy: () => ({
63
+ default: {
64
+ loginSignIn: "Se connecter",
65
+ loginContinueWith: "Continuer avec $1",
66
+ loginOr: "OU",
67
+ loginCancel: "Annuler",
68
+ loginForgotPassword: "Mot de passe oublié ?",
69
+ loginNoAccount: "Vous n'avez pas de compte ?",
70
+ loginSignUp: "S'inscrire",
71
+ loginUsername: "Nom d'utilisateur",
72
+ loginEmail: "E-mail",
73
+ loginPhone: "Numéro de téléphone",
74
+ loginPassword: "Mot de passe",
75
+ registerCreateAccount: "Créer un compte",
76
+ registerContinueWith: "Continuer avec $1",
77
+ registerOr: "OU",
78
+ registerCancel: "Annuler",
79
+ registerHaveAccount: "Vous avez déjà un compte ?",
80
+ registerSignIn: "Se connecter",
81
+ registerUsername: "Nom d'utilisateur",
82
+ registerEmail: "E-mail",
83
+ registerPhone: "Numéro de téléphone",
84
+ registerPassword: "Mot de passe",
85
+ registerConfirmPassword: "Confirmer le mot de passe",
86
+ registerDisabled:
87
+ "L'inscription n'est pas disponible. Veuillez contacter votre administrateur.",
88
+ registerBackToSignIn: "Retour à la connexion",
89
+ registerVerifyTitle: "Vérifiez votre compte",
90
+ registerVerifyDescription:
91
+ "Veuillez entrer le(s) code(s) de vérification qui vous ont été envoyés.",
92
+ registerEmailCode: "Code de vérification par e-mail",
93
+ registerPhoneCode: "Code de vérification par téléphone",
94
+ registerVerifySubmit: "Terminer l'inscription",
95
+ registerVerifyBack: "Retour à l'inscription",
96
+ resetPasswordTitle: "Réinitialiser le mot de passe",
97
+ resetPasswordEmail: "E-mail",
98
+ resetPasswordEnterEmail:
99
+ "Entrez votre adresse e-mail pour réinitialiser votre mot de passe",
100
+ resetPasswordSendCode: "Envoyer le code de vérification",
101
+ resetPasswordCodeSent:
102
+ "Nous avons envoyé un code de vérification à votre e-mail.",
103
+ resetPasswordEnterCode: "Entrez le code à 6 chiffres",
104
+ resetPasswordResendCode: "Renvoyer le code",
105
+ resetPasswordEnterNewPassword: "Créez votre nouveau mot de passe",
106
+ resetPasswordNewPassword: "Nouveau mot de passe",
107
+ resetPasswordConfirmPassword: "Confirmer le mot de passe",
108
+ resetPasswordSetNewPassword: "Définir le nouveau mot de passe",
109
+ resetPasswordSuccess:
110
+ "Votre mot de passe a été réinitialisé avec succès.",
111
+ resetPasswordBackToSignIn: "Retour à la connexion",
112
+ resetPasswordCancel: "Annuler",
113
+ resetPasswordDisabled:
114
+ "La réinitialisation du mot de passe n'est pas disponible. Veuillez contacter votre administrateur.",
115
+ },
116
+ }),
117
+ });
118
+ }
@@ -0,0 +1,53 @@
1
+ import { $page } from "@alepha/react";
2
+ import { t } from "alepha";
3
+ import type { UserRealmController } from "alepha/api/users";
4
+ import { $client } from "alepha/server/links";
5
+
6
+ export class AuthRouter {
7
+ userRealmClient = $client<UserRealmController>();
8
+
9
+ login = $page({
10
+ path: "/login",
11
+ schema: {
12
+ query: t.object({
13
+ redirect: t.optional(t.string()),
14
+ }),
15
+ },
16
+ lazy: () => import("./components/Login.tsx"),
17
+ resolve: async () => {
18
+ return {
19
+ realmConfig: await this.userRealmClient.getRealmConfig(),
20
+ };
21
+ },
22
+ });
23
+
24
+ register = $page({
25
+ path: "/register",
26
+ schema: {
27
+ query: t.object({
28
+ redirect: t.optional(t.string()),
29
+ }),
30
+ },
31
+ lazy: () => import("./components/Register.tsx"),
32
+ resolve: async () => {
33
+ return {
34
+ realmConfig: await this.userRealmClient.getRealmConfig(),
35
+ };
36
+ },
37
+ });
38
+
39
+ resetPassword = $page({
40
+ path: "/reset-password",
41
+ schema: {
42
+ query: t.object({
43
+ redirect: t.optional(t.string()),
44
+ }),
45
+ },
46
+ lazy: () => import("./components/ResetPassword.tsx"),
47
+ resolve: async () => {
48
+ return {
49
+ realmConfig: await this.userRealmClient.getRealmConfig(),
50
+ };
51
+ },
52
+ });
53
+ }
@@ -0,0 +1,193 @@
1
+ import { useRouter } from "@alepha/react";
2
+ import { useAuth } from "@alepha/react/auth";
3
+ import { FormValidationError, useForm } from "@alepha/react/form";
4
+ import { useI18n } from "@alepha/react/i18n";
5
+ import { ActionButton, Control, capitalize } from "@alepha/ui";
6
+ import { Card, Flex, Group, Stack, Text } from "@mantine/core";
7
+ import { IconLock, IconUser } from "@tabler/icons-react";
8
+ import { t } from "alepha";
9
+ import type { UserRealmConfig } from "alepha/api/users";
10
+ import { HttpError } from "alepha/server";
11
+ import { useMemo } from "react";
12
+ import type { AuthI18n } from "../AuthI18n.ts";
13
+ import type { AuthRouter } from "../AuthRouter.ts";
14
+ import IconGithub from "./icons/IconGithub.tsx";
15
+ import IconGoogle from "./icons/IconGoogle.tsx";
16
+
17
+ export interface LoginProps {
18
+ realmConfig: UserRealmConfig;
19
+ }
20
+
21
+ const Login = (props: LoginProps) => {
22
+ const auth = useAuth();
23
+ const router = useRouter<AuthRouter>();
24
+ const { tr } = useI18n<AuthI18n, "en">();
25
+ const redirect = router.query.redirect || "/";
26
+
27
+ const hasUsernamePassword = props.realmConfig.authenticationMethods.find(
28
+ (it) => it.type === "CREDENTIALS",
29
+ );
30
+
31
+ const settings = props.realmConfig.settings;
32
+
33
+ // Determine what login methods are available
34
+ const loginMethods = useMemo(() => {
35
+ const methods = [];
36
+ if (settings.usernameEnabled !== false) methods.push("username");
37
+ if (settings.emailEnabled !== false) methods.push("email");
38
+ if (settings.phoneEnabled === true) methods.push("phone");
39
+ return methods;
40
+ }, [settings]);
41
+
42
+ // Create identifier title based on enabled methods
43
+ const identifierTitle = useMemo(() => {
44
+ if (loginMethods.length === 0) return tr("loginUsername");
45
+ if (loginMethods.length === 1) {
46
+ if (loginMethods[0] === "username") return tr("loginUsername");
47
+ if (loginMethods[0] === "email") return tr("loginEmail");
48
+ if (loginMethods[0] === "phone") return tr("loginPhone");
49
+ }
50
+ const labels = loginMethods.map((m) => {
51
+ if (m === "username") return tr("loginUsername").toLowerCase();
52
+ if (m === "email") return tr("loginEmail").toLowerCase();
53
+ if (m === "phone") return tr("loginPhone").toLowerCase();
54
+ return m;
55
+ });
56
+ return capitalize(
57
+ `${labels.slice(0, -1).join(", ")} or ${labels[labels.length - 1]}`,
58
+ );
59
+ }, [loginMethods, tr]);
60
+
61
+ const form = useForm({
62
+ schema: t.object({
63
+ identifier: t.string({
64
+ minLength: 1,
65
+ }),
66
+ password: t.string({
67
+ minLength: settings.passwordPolicy?.minLength || 6,
68
+ }),
69
+ }),
70
+ handler: async (data) => {
71
+ try {
72
+ await auth.login("credentials", {
73
+ username: data.identifier,
74
+ password: data.password,
75
+ });
76
+ await router.go(router.query.r || "/");
77
+ } catch (error) {
78
+ if (
79
+ error instanceof HttpError &&
80
+ error.error === "InvalidCredentialsError"
81
+ ) {
82
+ throw new FormValidationError({
83
+ message: "Invalid identifier or password",
84
+ path: "/password",
85
+ });
86
+ }
87
+ throw error;
88
+ }
89
+ },
90
+ });
91
+
92
+ return (
93
+ <Flex flex={1} justify={"center"} align={"center"}>
94
+ <Stack gap={"sm"} w={360}>
95
+ <Card withBorder p={"lg"} bg={"var(--alepha-elevated)"}>
96
+ <Stack gap={"md"}>
97
+ {hasUsernamePassword && (
98
+ <>
99
+ <form {...form.props}>
100
+ <Stack flex={1} gap={"md"}>
101
+ <Control
102
+ title={identifierTitle}
103
+ input={form.input.identifier}
104
+ icon={IconUser}
105
+ text={{
106
+ autoComplete: loginMethods.includes("email")
107
+ ? "email"
108
+ : "username",
109
+ }}
110
+ />
111
+ <Control
112
+ title={tr("loginPassword")}
113
+ input={form.input.password}
114
+ icon={IconLock}
115
+ password={{
116
+ autoComplete: "current-password",
117
+ }}
118
+ />
119
+ <ActionButton variant={"filled"} form={form}>
120
+ {tr("loginSignIn")}
121
+ </ActionButton>
122
+ </Stack>
123
+ </form>
124
+ <Stack gap="xs">
125
+ {settings.resetPasswordAllowed && (
126
+ <Text size="sm" ta="center">
127
+ <ActionButton
128
+ href={router.path("resetPassword")}
129
+ anchorProps={{ inherit: true }}
130
+ >
131
+ {tr("loginForgotPassword")}
132
+ </ActionButton>
133
+ </Text>
134
+ )}
135
+ <Group align="center" justify="center" gap={"md"}>
136
+ <Flex flex={1} h={"1px"} bg={"var(--alepha-text-muted)"} />
137
+ <Text size="xs">{tr("loginOr")}</Text>
138
+ <Flex flex={1} h={"1px"} bg={"var(--alepha-text-muted)"} />
139
+ </Group>
140
+ </Stack>
141
+ </>
142
+ )}
143
+ <Stack gap={"sm"}>
144
+ {props.realmConfig.authenticationMethods.map(
145
+ (method) =>
146
+ method.type !== "CREDENTIALS" && (
147
+ <ActionButton
148
+ variant={"default"}
149
+ key={method.type}
150
+ leftSection={leftSection(method.name.toLowerCase())}
151
+ onClick={() =>
152
+ auth.login(method.name, {
153
+ redirect,
154
+ })
155
+ }
156
+ >
157
+ {tr("loginContinueWith", {
158
+ args: [capitalize(method.name)],
159
+ })}
160
+ </ActionButton>
161
+ ),
162
+ )}
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>
173
+ </Stack>
174
+ </Card>
175
+ <ActionButton variant={"subtle"} href={redirect}>
176
+ {tr("loginCancel")}
177
+ </ActionButton>
178
+ </Stack>
179
+ </Flex>
180
+ );
181
+ };
182
+
183
+ export default Login;
184
+
185
+ const leftSection = (name: string) => {
186
+ if (name === "google") {
187
+ return <IconGoogle />;
188
+ }
189
+
190
+ if (name === "github") {
191
+ return <IconGithub />;
192
+ }
193
+ };