@lastbrain/module-auth 2.0.27 → 2.0.31

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 (179) hide show
  1. package/README.md +55 -7
  2. package/dist/api/admin/signup-stats.d.ts.map +1 -1
  3. package/dist/api/admin/signup-stats.js +2 -1
  4. package/dist/api/admin/storage/usage.d.ts +18 -0
  5. package/dist/api/admin/storage/usage.d.ts.map +1 -0
  6. package/dist/api/admin/storage/usage.js +100 -0
  7. package/dist/api/admin/users/[id]/notifications.d.ts.map +1 -1
  8. package/dist/api/admin/users/[id]/notifications.js +3 -2
  9. package/dist/api/admin/users/[id].d.ts.map +1 -1
  10. package/dist/api/admin/users/[id].js +3 -2
  11. package/dist/api/admin/users/reactivate/[id].d.ts +16 -0
  12. package/dist/api/admin/users/reactivate/[id].d.ts.map +1 -0
  13. package/dist/api/admin/users/reactivate/[id].js +59 -0
  14. package/dist/api/admin/users/suspend/[id].d.ts +16 -0
  15. package/dist/api/admin/users/suspend/[id].d.ts.map +1 -0
  16. package/dist/api/admin/users/suspend/[id].js +59 -0
  17. package/dist/api/admin/users-by-source.d.ts.map +1 -1
  18. package/dist/api/admin/users-by-source.js +2 -1
  19. package/dist/api/admin/users.d.ts.map +1 -1
  20. package/dist/api/admin/users.js +53 -2
  21. package/dist/api/auth/account/email-change.d.ts +7 -0
  22. package/dist/api/auth/account/email-change.d.ts.map +1 -0
  23. package/dist/api/auth/account/email-change.js +39 -0
  24. package/dist/api/auth/account/reset-password.d.ts +7 -0
  25. package/dist/api/auth/account/reset-password.d.ts.map +1 -0
  26. package/dist/api/auth/account/reset-password.js +36 -0
  27. package/dist/api/auth/check-username.d.ts +9 -0
  28. package/dist/api/auth/check-username.d.ts.map +1 -0
  29. package/dist/api/auth/check-username.js +35 -0
  30. package/dist/api/auth/establish-session.d.ts +2 -0
  31. package/dist/api/auth/establish-session.d.ts.map +1 -0
  32. package/dist/api/auth/establish-session.js +23 -0
  33. package/dist/api/auth/me.d.ts +4 -4
  34. package/dist/api/auth/me.d.ts.map +1 -1
  35. package/dist/api/auth/me.js +28 -6
  36. package/dist/api/auth/profile.d.ts.map +1 -1
  37. package/dist/api/auth/profile.js +6 -3
  38. package/dist/api/auth/storage/recalculate.d.ts +15 -0
  39. package/dist/api/auth/storage/recalculate.d.ts.map +1 -0
  40. package/dist/api/auth/storage/recalculate.js +68 -0
  41. package/dist/api/auth/storage/usage.d.ts +10 -0
  42. package/dist/api/auth/storage/usage.d.ts.map +1 -0
  43. package/dist/api/auth/storage/usage.js +86 -0
  44. package/dist/api/public/add-welcome-bonus.d.ts +16 -0
  45. package/dist/api/public/add-welcome-bonus.d.ts.map +1 -0
  46. package/dist/api/public/add-welcome-bonus.js +177 -0
  47. package/dist/api/public/callback.d.ts +3 -0
  48. package/dist/api/public/callback.d.ts.map +1 -0
  49. package/dist/api/public/callback.js +197 -0
  50. package/dist/api/public/reset-password.d.ts +3 -0
  51. package/dist/api/public/reset-password.d.ts.map +1 -0
  52. package/dist/api/public/reset-password.js +43 -0
  53. package/dist/api/public/set-session.d.ts +7 -0
  54. package/dist/api/public/set-session.d.ts.map +1 -0
  55. package/dist/api/public/set-session.js +55 -0
  56. package/dist/api/public/signin.d.ts.map +1 -1
  57. package/dist/api/public/signin.js +31 -0
  58. package/dist/api/public/signup.d.ts.map +1 -1
  59. package/dist/api/public/signup.js +38 -27
  60. package/dist/api/public/webhook/storage-addon.d.ts +9 -0
  61. package/dist/api/public/webhook/storage-addon.d.ts.map +1 -0
  62. package/dist/api/public/webhook/storage-addon.js +155 -0
  63. package/dist/api/storage.js +2 -2
  64. package/dist/auth.build.config.d.ts.map +1 -1
  65. package/dist/auth.build.config.js +126 -11
  66. package/dist/components/AccountButton.d.ts.map +1 -1
  67. package/dist/components/AccountButton.js +54 -28
  68. package/dist/components/Doc.d.ts.map +1 -1
  69. package/dist/components/Doc.js +1 -1
  70. package/dist/components/HasProfil.d.ts +4 -0
  71. package/dist/components/HasProfil.d.ts.map +1 -0
  72. package/dist/components/HasProfil.js +39 -0
  73. package/dist/components/auth/dashboard.d.ts +1 -1
  74. package/dist/components/auth/dashboard.d.ts.map +1 -1
  75. package/dist/components/auth/dashboard.js +34 -7
  76. package/dist/index.d.ts +2 -0
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +2 -0
  79. package/dist/lib/app-branding-data.d.ts +22 -0
  80. package/dist/lib/app-branding-data.d.ts.map +1 -0
  81. package/dist/lib/app-branding-data.js +49 -0
  82. package/dist/lib/auth-email-service.d.ts +57 -0
  83. package/dist/lib/auth-email-service.d.ts.map +1 -0
  84. package/dist/lib/auth-email-service.js +382 -0
  85. package/dist/lib/auth-email-templates.d.ts +2 -0
  86. package/dist/lib/auth-email-templates.d.ts.map +1 -0
  87. package/dist/lib/auth-email-templates.js +1 -0
  88. package/dist/lib/site-url.d.ts +3 -0
  89. package/dist/lib/site-url.d.ts.map +1 -0
  90. package/dist/lib/site-url.js +11 -0
  91. package/dist/sitemap/manifest.d.ts +9 -0
  92. package/dist/sitemap/manifest.d.ts.map +1 -0
  93. package/dist/sitemap/manifest.js +14 -0
  94. package/dist/web/admin/signup-stats.js +3 -3
  95. package/dist/web/admin/user-detail.d.ts.map +1 -1
  96. package/dist/web/admin/user-detail.js +135 -14
  97. package/dist/web/admin/users-by-signup-source.js +2 -2
  98. package/dist/web/admin/users.d.ts.map +1 -1
  99. package/dist/web/admin/users.js +26 -7
  100. package/dist/web/auth/folder.d.ts.map +1 -1
  101. package/dist/web/auth/folder.js +4 -3
  102. package/dist/web/auth/profile.d.ts.map +1 -1
  103. package/dist/web/auth/profile.js +132 -13
  104. package/dist/web/auth/reglage.d.ts.map +1 -1
  105. package/dist/web/auth/reglage.js +15 -8
  106. package/dist/web/public/ResetPassword.d.ts.map +1 -1
  107. package/dist/web/public/ResetPassword.js +172 -2
  108. package/dist/web/public/SignInPage.d.ts.map +1 -1
  109. package/dist/web/public/SignInPage.js +39 -3
  110. package/dist/web/public/SignUpPage.d.ts.map +1 -1
  111. package/dist/web/public/SignUpPage.js +7 -2
  112. package/dist/web/public/auth-code-error.d.ts +2 -0
  113. package/dist/web/public/auth-code-error.d.ts.map +1 -0
  114. package/dist/web/public/auth-code-error.js +14 -0
  115. package/dist/web/public/confirm.d.ts +2 -0
  116. package/dist/web/public/confirm.d.ts.map +1 -0
  117. package/dist/web/public/confirm.js +157 -0
  118. package/package.json +10 -5
  119. package/src/api/admin/signup-stats.ts +2 -1
  120. package/src/api/admin/storage/usage.ts +141 -0
  121. package/src/api/admin/users/[id]/notifications.ts +3 -2
  122. package/src/api/admin/users/[id].ts +3 -2
  123. package/src/api/admin/users/reactivate/[id].ts +88 -0
  124. package/src/api/admin/users/suspend/[id].ts +85 -0
  125. package/src/api/admin/users-by-source.ts +2 -1
  126. package/src/api/admin/users.ts +59 -2
  127. package/src/api/auth/account/email-change.ts +54 -0
  128. package/src/api/auth/account/reset-password.ts +47 -0
  129. package/src/api/auth/check-username.ts +52 -0
  130. package/src/api/auth/establish-session.ts +32 -0
  131. package/src/api/auth/me.ts +29 -7
  132. package/src/api/auth/profile.ts +6 -2
  133. package/src/api/auth/storage/recalculate.ts +108 -0
  134. package/src/api/auth/storage/usage.ts +113 -0
  135. package/src/api/public/add-welcome-bonus.ts +229 -0
  136. package/src/api/public/callback.ts +307 -0
  137. package/src/api/public/reset-password.ts +52 -0
  138. package/src/api/public/set-session.ts +73 -0
  139. package/src/api/public/signin.ts +36 -0
  140. package/src/api/public/signup.ts +44 -37
  141. package/src/api/public/webhook/storage-addon.ts +267 -0
  142. package/src/api/storage.ts +2 -2
  143. package/src/auth.build.config.ts +126 -11
  144. package/src/components/AccountButton.tsx +114 -90
  145. package/src/components/Doc.tsx +47 -9
  146. package/src/components/HasProfil.tsx +63 -0
  147. package/src/components/auth/dashboard.tsx +54 -13
  148. package/src/i18n/en.json +76 -8
  149. package/src/i18n/es.json +330 -0
  150. package/src/i18n/fr.json +74 -8
  151. package/src/index.ts +2 -0
  152. package/src/lib/app-branding-data.ts +90 -0
  153. package/src/lib/auth-email-service.ts +508 -0
  154. package/src/lib/auth-email-templates.ts +5 -0
  155. package/src/lib/site-url.ts +17 -0
  156. package/src/sitemap/manifest.ts +26 -0
  157. package/src/web/admin/signup-stats.tsx +3 -3
  158. package/src/web/admin/user-detail.tsx +314 -15
  159. package/src/web/admin/users-by-signup-source.tsx +2 -2
  160. package/src/web/admin/users.tsx +50 -14
  161. package/src/web/auth/folder.tsx +23 -5
  162. package/src/web/auth/profile.tsx +227 -13
  163. package/src/web/auth/reglage.tsx +55 -24
  164. package/src/web/public/ResetPassword.tsx +301 -1
  165. package/src/web/public/SignInPage.tsx +43 -3
  166. package/src/web/public/SignUpPage.tsx +14 -5
  167. package/src/web/public/auth-code-error.tsx +49 -0
  168. package/src/web/public/confirm.tsx +195 -0
  169. package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +3 -1
  170. package/supabase/migrations/20251127100000_rename_body_to_message.sql +8 -3
  171. package/supabase/migrations/20260120150001_add_user_storage.sql +105 -0
  172. package/supabase/migrations/20260122131200_add_global_addons_system.sql +305 -0
  173. package/supabase/migrations/20260123100000_enable_vector_extension.sql +9 -0
  174. package/supabase/migrations/20260123140000_module_auth_fix_get_user_limits_null.sql +93 -0
  175. package/supabase/migrations/20260123145000_module_auth_fix_circular_storage_base.sql +89 -0
  176. package/supabase/migrations/20260129400000_add_username_to_user_profil.sql +33 -0
  177. package/dist/web/auth/dashboard.d.ts +0 -2
  178. package/dist/web/auth/dashboard.d.ts.map +0 -1
  179. package/dist/web/auth/dashboard.js +0 -48
@@ -1,3 +1,303 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Alert, Button, Card, CardBody, Input, addToast } from "@lastbrain/ui";
5
+ import { Mail, ArrowRight, Lock } from "lucide-react";
6
+ import {
7
+ useModuleTranslation,
8
+ useLocalizedRouter,
9
+ useLanguage,
10
+ logger,
11
+ supabaseBrowserClient,
12
+ } from "@lastbrain/core";
13
+
1
14
  export function ResetPassword() {
2
- return <div>Reset Password Page</div>;
15
+ const t = useModuleTranslation("auth");
16
+ const router = useLocalizedRouter();
17
+ const { lang } = useLanguage();
18
+ const [email, setEmail] = useState("");
19
+ const [loading, setLoading] = useState(false);
20
+ const [success, setSuccess] = useState<string | null>(null);
21
+ const [hasToken, setHasToken] = useState(false);
22
+ const [newPassword, setNewPassword] = useState("");
23
+ const [confirmPassword, setConfirmPassword] = useState("");
24
+ const [expiredMessage, setExpiredMessage] = useState<string | null>(null);
25
+
26
+ // Detect access_token in hash (Supabase recovery link) and set session
27
+ useEffect(() => {
28
+ logger.debug("[ResetPassword] checking for tokens in URL hash");
29
+ const hash = window.location.hash.replace(/^#/, "");
30
+ if (!hash) return;
31
+ const hasLangPrefix = /^\/[a-z]{2}(?:\/|$)/.test(window.location.pathname);
32
+ if (!hasLangPrefix) {
33
+ const preferredLang = lang || "fr";
34
+ const pathWithLang = window.location.pathname.startsWith("/")
35
+ ? window.location.pathname
36
+ : `/${window.location.pathname}`;
37
+ const target = `/${preferredLang}${pathWithLang}${
38
+ window.location.search || ""
39
+ }${window.location.hash || ""}`;
40
+ window.location.replace(target);
41
+ return;
42
+ }
43
+
44
+ const params = new URLSearchParams(hash);
45
+ const error = params.get("error");
46
+ const errorDescription = params.get("error_description");
47
+ if (error) {
48
+ const message =
49
+ errorDescription ||
50
+ t("reset_password.link_expired") ||
51
+ "Lien invalide ou expiré, demandez un nouveau lien.";
52
+ setExpiredMessage(message);
53
+ logger.warn("Reset password link error", {
54
+ error,
55
+ description: errorDescription,
56
+ });
57
+ console.warn("Reset password magic link error", {
58
+ error,
59
+ errorDescription,
60
+ });
61
+ addToast({
62
+ title: t("reset_password.error") || "Erreur",
63
+ description: message,
64
+ color: "warning",
65
+ });
66
+ window.history.replaceState(
67
+ {},
68
+ document.title,
69
+ window.location.pathname + window.location.search
70
+ );
71
+ return;
72
+ }
73
+
74
+ const access_token = params.get("access_token");
75
+ const refresh_token = params.get("refresh_token");
76
+
77
+ if (access_token && refresh_token) {
78
+ supabaseBrowserClient.auth
79
+ .setSession({ access_token, refresh_token })
80
+ .then(async ({ error }) => {
81
+ if (error) {
82
+ addToast({
83
+ title: t("reset_password.error") || "Erreur",
84
+ description: error.message,
85
+ color: "danger",
86
+ });
87
+ return;
88
+ }
89
+
90
+ try {
91
+ const setSessionRes = await fetch("/api/public/set-session", {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify({ access_token, refresh_token }),
95
+ });
96
+ if (!setSessionRes.ok) {
97
+ const setSessionBody = await setSessionRes
98
+ .json()
99
+ .catch(() => null);
100
+ logger.info("Failed to persist session cookies", setSessionBody);
101
+ }
102
+ } catch (err) {
103
+ logger.info("set-session call failed", err);
104
+ }
105
+
106
+ setHasToken(true);
107
+ // Clear hash to avoid leaking tokens
108
+ window.history.replaceState(
109
+ {},
110
+ document.title,
111
+ window.location.pathname + window.location.search
112
+ );
113
+ });
114
+ }
115
+ }, [t, lang]);
116
+
117
+ const handleRequestLink = async (e: React.FormEvent) => {
118
+ e.preventDefault();
119
+ setSuccess(null);
120
+ setLoading(true);
121
+
122
+ try {
123
+ const response = await fetch("/api/public/reset-password", {
124
+ method: "POST",
125
+ headers: {
126
+ "Content-Type": "application/json",
127
+ },
128
+ body: JSON.stringify({ email }),
129
+ });
130
+
131
+ const data = await response.json();
132
+ if (!response.ok) {
133
+ throw new Error(data.error || "Erreur lors de l'envoi du lien");
134
+ }
135
+
136
+ setSuccess(
137
+ t("reset_password.success") ||
138
+ "Vérifiez votre email pour le lien de réinitialisation"
139
+ );
140
+ } catch (error: any) {
141
+ addToast({
142
+ title: t("reset_password.error") || "Erreur",
143
+ description:
144
+ error?.message ||
145
+ t("reset_password.error") ||
146
+ "Impossible d'envoyer le lien de réinitialisation",
147
+ color: "danger",
148
+ });
149
+ } finally {
150
+ setLoading(false);
151
+ }
152
+ };
153
+
154
+ const handleUpdatePassword = async (e: React.FormEvent) => {
155
+ e.preventDefault();
156
+ if (newPassword.length < 6) {
157
+ addToast({
158
+ title: t("reset_password.error") || "Erreur",
159
+ description:
160
+ t("signup.password_too_short") || "Mot de passe trop court",
161
+ color: "warning",
162
+ });
163
+ return;
164
+ }
165
+ if (newPassword !== confirmPassword) {
166
+ addToast({
167
+ title: t("reset_password.error") || "Erreur",
168
+ description:
169
+ t("signup.password_mismatch") ||
170
+ "Les mots de passe ne correspondent pas.",
171
+ color: "warning",
172
+ });
173
+ return;
174
+ }
175
+
176
+ setLoading(true);
177
+ try {
178
+ const { error } = await supabaseBrowserClient.auth.updateUser({
179
+ password: newPassword,
180
+ });
181
+
182
+ if (error) throw error;
183
+
184
+ addToast({
185
+ title: t("reset_password.success") || "Succès",
186
+ description:
187
+ t("profile.security_reset_desc") || "Mot de passe mis à jour",
188
+ color: "success",
189
+ });
190
+
191
+ setTimeout(() => router.push("/auth/dashboard"), 1200);
192
+ } catch (err: any) {
193
+ addToast({
194
+ title: t("reset_password.error") || "Erreur",
195
+ description:
196
+ err?.message || "Impossible de mettre à jour le mot de passe",
197
+ color: "danger",
198
+ });
199
+ } finally {
200
+ setLoading(false);
201
+ }
202
+ };
203
+
204
+ return (
205
+ <div className="min-h-[40vh] flex items-center justify-center px-4 py-16 mt-16 ">
206
+ <div className="w-full max-w-md">
207
+ <div className="text-center mb-8">
208
+ <h1 className="text-3xl font-bold mb-2">
209
+ {t("reset_password.title") || "Réinitialiser le mot de passe"}
210
+ </h1>
211
+ <p className="text-default-600 dark:text-default-500">
212
+ {t("reset_password.subtitle") ||
213
+ "Entrez votre email pour recevoir un lien"}
214
+ </p>
215
+ </div>
216
+
217
+ <Card className="">
218
+ <CardBody className="p-6 space-y-6">
219
+ {expiredMessage && (
220
+ <Alert
221
+ color="warning"
222
+ title={t("reset_password.link_expired_title") || "Lien expiré"}
223
+ className="text-sm font-semibold"
224
+ >
225
+ {expiredMessage}
226
+ </Alert>
227
+ )}
228
+ {hasToken ? (
229
+ <form onSubmit={handleUpdatePassword} className="space-y-4">
230
+ <Input
231
+ label={t("signup.password") || "Nouveau mot de passe"}
232
+ type="password"
233
+ value={newPassword}
234
+ onChange={(e) => setNewPassword(e.target.value)}
235
+ startContent={<Lock className="w-4 h-4 text-default-400" />}
236
+ isRequired
237
+ />
238
+ <Input
239
+ label={t("signup.confirm_password") || "Confirmer"}
240
+ type="password"
241
+ value={confirmPassword}
242
+ onChange={(e) => setConfirmPassword(e.target.value)}
243
+ startContent={<Lock className="w-4 h-4 text-default-400" />}
244
+ isRequired
245
+ />
246
+ <Button
247
+ type="submit"
248
+ color="primary"
249
+ isLoading={loading}
250
+ endContent={!loading && <ArrowRight className="w-4 h-4" />}
251
+ className="w-full"
252
+ >
253
+ {t("reset_password.submit") ||
254
+ "Mettre à jour le mot de passe"}
255
+ </Button>
256
+ </form>
257
+ ) : success ? (
258
+ <div className="text-center text-success font-semibold">
259
+ {success}
260
+ </div>
261
+ ) : (
262
+ <form onSubmit={handleRequestLink} className="space-y-6">
263
+ <Input
264
+ label={
265
+ t("reset_password.title") || "Réinitialiser le mot de passe"
266
+ }
267
+ placeholder={
268
+ t("reset_password.email_placeholder") || "votre@email.com"
269
+ }
270
+ type="email"
271
+ value={email}
272
+ onChange={(e) => setEmail(e.target.value)}
273
+ startContent={<Mail className="w-4 h-4 text-default-400" />}
274
+ isRequired
275
+ />
276
+
277
+ <Button
278
+ type="submit"
279
+ color="primary"
280
+ isLoading={loading}
281
+ endContent={!loading && <ArrowRight className="w-4 h-4" />}
282
+ className="w-full"
283
+ >
284
+ {t("reset_password.submit") || "Envoyer le lien"}
285
+ </Button>
286
+
287
+ <Button
288
+ type="button"
289
+ variant="flat"
290
+ onPress={() => router.push("/signin")}
291
+ className="w-full"
292
+ >
293
+ {t("reset_password.back_to_signin") ||
294
+ "Retour à la connexion"}
295
+ </Button>
296
+ </form>
297
+ )}
298
+ </CardBody>
299
+ </Card>
300
+ </div>
301
+ </div>
302
+ );
3
303
  }
@@ -52,6 +52,46 @@ function SignInForm() {
52
52
 
53
53
  setIsLoading(false);
54
54
 
55
+ // Si le compte est suspendu, afficher un toast et déconnecter l'utilisateur
56
+ const isSuspended =
57
+ data?.user?.app_metadata?.suspended ??
58
+ (data?.user as any)?.raw_app_meta_data?.suspended ??
59
+ false;
60
+
61
+ if (isSuspended) {
62
+ addToast({
63
+ color: "danger",
64
+ title: t("signin.suspended_title") || "Compte suspendu",
65
+ description:
66
+ t("signin.suspended_message") ||
67
+ "Votre compte a été suspendu. Contactez l'administrateur.",
68
+ });
69
+ try {
70
+ await supabaseBrowserClient.auth.signOut();
71
+ } catch (e) {
72
+ // ignore
73
+ }
74
+ return;
75
+ }
76
+
77
+ // Appeler l'endpoint serveur pour synchroniser le cookie `NEXT_LOCALE`.
78
+ // L'endpoint mettra à jour le cookie seulement s'il existe côté client
79
+ // (ex: cookie HttpOnly envoyé avec la requête).
80
+ // try {
81
+ // const res = await fetch("/api/auth/sync-locale", {
82
+ // method: "POST",
83
+ // credentials: "include",
84
+ // headers: { "Content-Type": "application/json" },
85
+ // body: JSON.stringify({ userId: data.user?.id }),
86
+ // });
87
+ // // Wait for body to be consumed so the Set-Cookie is processed by the browser
88
+ // if (res && res.body) {
89
+ // await res.text().catch(() => null);
90
+ // }
91
+ // } catch (e) {
92
+ // console.debug("signin: sync-locale request failed", e);
93
+ // }
94
+
55
95
  addToast({
56
96
  color: "success",
57
97
  title: t("signin.success_title") || "Connecté avec succès !",
@@ -69,7 +109,7 @@ function SignInForm() {
69
109
  };
70
110
 
71
111
  return (
72
- <div className="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-primary-50/30 to-secondary-50/30 dark:from-primary-950/50 dark:to-secondary-950/50">
112
+ <div className="pt-16 relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-primary-50/30 to-secondary-50/30 dark:from-primary-950/50 dark:to-secondary-950/50">
73
113
  {/* Background decoration */}
74
114
  <div className="absolute inset-0 bg-grid-slate-100 dark:bg-grid-slate-800/20 mask-[linear-gradient(0deg,white,rgba(255,255,255,0.6))] dark:mask-[linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.05))]" />
75
115
 
@@ -97,7 +137,7 @@ function SignInForm() {
97
137
  </div>
98
138
 
99
139
  {/* Form Card */}
100
- <Card className="border border-default-200/60 bg-background/60 backdrop-blur-md backdrop-saturate-150 shadow-xl">
140
+ <Card className="border border-default-200/60 backdrop-blur-md backdrop-saturate-150 shadow-xl">
101
141
  <CardBody className="gap-6 p-8">
102
142
  <form onSubmit={handleSubmit} className="flex flex-col gap-6">
103
143
  <Input
@@ -134,7 +174,7 @@ function SignInForm() {
134
174
  }}
135
175
  />
136
176
  <Link
137
- href="/forgot-password"
177
+ href="/reset-password"
138
178
  className="text-xs text-default-500 hover:text-primary-500 self-end"
139
179
  >
140
180
  {t("signin.forgot_password") || "Mot de passe oublié ?"}
@@ -10,7 +10,7 @@ import {
10
10
  addToast,
11
11
  } from "@lastbrain/ui";
12
12
  import { useSearchParams } from "next/navigation";
13
- import { Suspense, useState } from "react";
13
+ import { SetStateAction, Suspense, useState } from "react";
14
14
  import {
15
15
  Mail,
16
16
  Lock,
@@ -87,6 +87,11 @@ function SignUpForm() {
87
87
  setSuccess(
88
88
  "Compte créé avec succès ! Veuillez vérifier votre email pour confirmer votre compte."
89
89
  );
90
+ // Reset le formulaire après succès
91
+ setFullName("");
92
+ setEmail("");
93
+ setPassword("");
94
+ setConfirmPassword("");
90
95
  return;
91
96
  }
92
97
 
@@ -110,7 +115,7 @@ function SignUpForm() {
110
115
  };
111
116
 
112
117
  return (
113
- <div className=" relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-secondary-50/30 to-primary-50/30 dark:from-secondary-950/50 dark:to-primary-950/50">
118
+ <div className="pt-16 relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-secondary-50/30 to-primary-50/30 dark:from-secondary-950/50 dark:to-primary-950/50">
114
119
  {/* Background decoration */}
115
120
  <div className="absolute inset-0 bg-grid-slate-100 dark:bg-grid-slate-800/20 mask-[linear-gradient(0deg,white,rgba(255,255,255,0.6))] dark:mask-[linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.05))]" />
116
121
 
@@ -138,7 +143,7 @@ function SignUpForm() {
138
143
  </div>
139
144
 
140
145
  {/* Form Card */}
141
- <Card className="border border-default-200/60 bg-background/60 backdrop-blur-md backdrop-saturate-150 shadow-xl">
146
+ <Card className="border border-default-200/60 backdrop-blur-md backdrop-saturate-150 shadow-xl">
142
147
  <CardBody className="gap-6 p-8">
143
148
  <form onSubmit={handleSubmit} className="flex flex-col gap-6">
144
149
  <Input
@@ -164,7 +169,9 @@ function SignUpForm() {
164
169
  t("signin.email_placeholder") || "votre@email.com"
165
170
  }
166
171
  value={email}
167
- onChange={(e) => setEmail(e.target.value)}
172
+ onChange={(e: {
173
+ target: { value: SetStateAction<string> };
174
+ }) => setEmail(e.target.value)}
168
175
  required
169
176
  startContent={<Mail className="h-4 w-4 text-default-400" />}
170
177
  classNames={{
@@ -200,7 +207,9 @@ function SignUpForm() {
200
207
  type="password"
201
208
  placeholder="••••••••"
202
209
  value={confirmPassword}
203
- onChange={(e) => setConfirmPassword(e.target.value)}
210
+ onChange={(e: {
211
+ target: { value: SetStateAction<string> };
212
+ }) => setConfirmPassword(e.target.value)}
204
213
  required
205
214
  minLength={6}
206
215
  startContent={<Lock className="h-4 w-4 text-default-400" />}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Auth error page - shown when email confirmation fails
3
+ */
4
+ "use client";
5
+
6
+ import { useSearchParams } from "next/navigation";
7
+ import Link from "next/link";
8
+
9
+ export default function AuthCodeErrorPage() {
10
+ const searchParams = useSearchParams();
11
+ const error = searchParams.get("error");
12
+ const description = searchParams.get("description");
13
+
14
+ return (
15
+ <div className="flex items-center justify-center min-h-screen bg-gray-50">
16
+ <div className="w-full max-w-md p-8 bg-white rounded-lg shadow-md">
17
+ <div className="text-center">
18
+ <div className="text-red-500 text-4xl mb-4">❌</div>
19
+ <h1 className="text-2xl font-bold text-gray-900 mb-2">
20
+ Confirmation Failed
21
+ </h1>
22
+ <p className="text-gray-600 mb-4">
23
+ {description ||
24
+ "We couldn't confirm your email. The link may have expired."}
25
+ </p>
26
+ {error && (
27
+ <p className="text-sm text-gray-500 bg-gray-100 p-2 rounded mb-6">
28
+ Error: {error}
29
+ </p>
30
+ )}
31
+ <div className="space-y-3">
32
+ <Link
33
+ href="/auth/signin"
34
+ className="block w-full px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition"
35
+ >
36
+ Try signing in
37
+ </Link>
38
+ <Link
39
+ href="/auth/signup"
40
+ className="block w-full px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition"
41
+ >
42
+ Create new account
43
+ </Link>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Email confirmation handler
3
+ * Handles hash-based tokens (direct from email with #access_token=XXX)
4
+ *
5
+ * Note: Code-based flow is now handled server-side in /api/auth/callback
6
+ * This page is only reached for hash-based tokens or as a fallback loading screen
7
+ */
8
+ "use client";
9
+
10
+ import { useEffect, useState } from "react";
11
+ import { useSearchParams } from "next/navigation";
12
+ import { addToast, Card, Spinner } from "@lastbrain/ui";
13
+ import { logger, useLocalizedRouter } from "@lastbrain/core";
14
+
15
+ export default function ConfirmPage() {
16
+ const router = useLocalizedRouter();
17
+ const searchParams = useSearchParams();
18
+ const [message, setMessage] = useState("Confirming your email...");
19
+ const [isProcessing, setIsProcessing] = useState(false);
20
+ const [debug, setDebug] = useState<{
21
+ href?: string | null;
22
+ hash?: string | null;
23
+ access_token?: string | null;
24
+ refresh_token?: string | null;
25
+ responseStatus?: number | null;
26
+ responseBody?: any;
27
+ } | null>(null);
28
+
29
+ useEffect(() => {
30
+ // Éviter les appels multiples
31
+ if (isProcessing) return;
32
+
33
+ const confirmEmail = async () => {
34
+ setIsProcessing(true);
35
+ const next = searchParams.get("next") || "/";
36
+
37
+ // Check for token in hash (direct from email links that use #access_token)
38
+ // This handles the legacy hash-based flow from Supabase
39
+ if (typeof window !== "undefined") {
40
+ const hash = window.location.hash;
41
+
42
+ if (hash && hash.length > 1) {
43
+ const params = new URLSearchParams(hash.substring(1));
44
+ const accessToken = params.get("access_token");
45
+ const refreshToken = params.get("refresh_token");
46
+
47
+ if (accessToken && refreshToken) {
48
+ try {
49
+ setMessage("Setting up your session...");
50
+
51
+ // Debug: capture full href & hash and tokens for troubleshooting
52
+ setDebug({
53
+ href: window.location.href,
54
+ hash,
55
+ access_token: accessToken,
56
+ refresh_token: refreshToken,
57
+ });
58
+
59
+ // Sync tokens to server-side cookies
60
+ console.debug(
61
+ "[ConfirmPage] Posting tokens to /api/public/set-session",
62
+ {
63
+ accessToken: String(accessToken).slice(0, 8) + "...",
64
+ refreshToken: String(refreshToken).slice(0, 8) + "...",
65
+ }
66
+ );
67
+ const response = await fetch("/api/public/set-session", {
68
+ method: "POST",
69
+ headers: { "Content-Type": "application/json" },
70
+ body: JSON.stringify({
71
+ access_token: accessToken,
72
+ refresh_token: refreshToken,
73
+ }),
74
+ });
75
+ const respBody = await response.json().catch(() => null);
76
+ console.debug("[ConfirmPage] /api/public/set-session response", {
77
+ ok: response.ok,
78
+ status: response.status,
79
+ body: respBody,
80
+ });
81
+
82
+ // Update debug panel with response
83
+ setDebug((d) => ({
84
+ ...(d || {}),
85
+ responseStatus: response.status,
86
+ responseBody: respBody,
87
+ }));
88
+
89
+ logger.debug("[ConfirmPage] start", {
90
+ url:
91
+ typeof window !== "undefined" ? window.location.href : null,
92
+ searchParams: String(searchParams),
93
+ next,
94
+ });
95
+ if (!response.ok) {
96
+ const errorData = await response.json();
97
+ throw new Error(errorData.error || "Failed to set session");
98
+ }
99
+
100
+ // Add welcome bonus asynchronously (don't block redirect)
101
+ try {
102
+ const res = await fetch("/api/public/add-welcome-bonus", {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ });
106
+ if (!res.ok) {
107
+ const err = await res.json().catch(() => ({}));
108
+ logger.warn("add-welcome-bonus failed:", err);
109
+ } else {
110
+ logger.debug("add-welcome-bonus succeeded for user");
111
+ }
112
+ } catch (bonusError) {
113
+ logger.warn("Failed to add welcome bonus:", bonusError);
114
+ // Don't block the redirect if bonus fails
115
+ }
116
+
117
+ addToast({
118
+ title: "Email confirmed!",
119
+ description: "Your email has been verified successfully.",
120
+ color: "success",
121
+ });
122
+
123
+ // Redirect to destination
124
+ // Use hard reload to ensure cookies are applied
125
+ setMessage("Redirecting...");
126
+ setTimeout(() => {
127
+ window.location.href = next;
128
+ }, 500);
129
+ return;
130
+ } catch (err) {
131
+ logger.error("❌ Error setting session:", err);
132
+ addToast({
133
+ title: "Email confirmation failed",
134
+ description:
135
+ err instanceof Error
136
+ ? err.message
137
+ : "Could not create session",
138
+ color: "danger",
139
+ });
140
+ router.push("/auth-code-error");
141
+ return;
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ // If we reach here, either:
148
+ // 1. Code was already handled by callback (just show loading and redirect)
149
+ // 2. No valid token/code found (redirect to error)
150
+
151
+ const code = searchParams.get("code");
152
+
153
+ if (code) {
154
+ // Code should have been handled by callback, but we're here
155
+ // This means callback already set cookies and redirected us here as fallback
156
+ // Just redirect to next
157
+ setMessage("Completing sign-in...");
158
+ setTimeout(() => {
159
+ window.location.href = next;
160
+ }, 500);
161
+ return;
162
+ }
163
+
164
+ // No code or token found - assume the user was redirected here after
165
+ // a verification step (Supabase verify) which does not provide a code.
166
+ // Consider this a successful confirmation and redirect to `next`.
167
+ logger.debug(
168
+ "No confirmation code or token found - assuming success and redirecting"
169
+ );
170
+ addToast({
171
+ title: "Email confirmé",
172
+ description: "Votre email a été vérifié.",
173
+ color: "success",
174
+ });
175
+ setMessage("Redirecting...");
176
+ setTimeout(() => {
177
+ window.location.href = next;
178
+ }, 500);
179
+ };
180
+
181
+ confirmEmail();
182
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
183
+ // On ne met pas searchParams/router dans les deps pour éviter les boucles
184
+
185
+ return (
186
+ <div className="flex items-center justify-center min-h-screen ">
187
+ <Card className="w-full max-w-md">
188
+ <div className="text-center py-8">
189
+ <Spinner size="lg" className="mx-auto" />
190
+ <p className="mt-4 text-gray-600">{message}</p>
191
+ </div>
192
+ </Card>
193
+ </div>
194
+ );
195
+ }