@lastbrain/module-auth 2.0.19 → 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.
- package/README.md +55 -7
- package/dist/api/admin/signup-stats.d.ts.map +1 -1
- package/dist/api/admin/signup-stats.js +2 -1
- package/dist/api/admin/storage/usage.d.ts +18 -0
- package/dist/api/admin/storage/usage.d.ts.map +1 -0
- package/dist/api/admin/storage/usage.js +100 -0
- package/dist/api/admin/users/[id]/notifications.d.ts.map +1 -1
- package/dist/api/admin/users/[id]/notifications.js +3 -2
- package/dist/api/admin/users/[id].d.ts.map +1 -1
- package/dist/api/admin/users/[id].js +3 -2
- package/dist/api/admin/users/reactivate/[id].d.ts +16 -0
- package/dist/api/admin/users/reactivate/[id].d.ts.map +1 -0
- package/dist/api/admin/users/reactivate/[id].js +59 -0
- package/dist/api/admin/users/suspend/[id].d.ts +16 -0
- package/dist/api/admin/users/suspend/[id].d.ts.map +1 -0
- package/dist/api/admin/users/suspend/[id].js +59 -0
- package/dist/api/admin/users-by-source.d.ts.map +1 -1
- package/dist/api/admin/users-by-source.js +2 -1
- package/dist/api/admin/users.d.ts.map +1 -1
- package/dist/api/admin/users.js +53 -2
- package/dist/api/auth/account/email-change.d.ts +7 -0
- package/dist/api/auth/account/email-change.d.ts.map +1 -0
- package/dist/api/auth/account/email-change.js +39 -0
- package/dist/api/auth/account/reset-password.d.ts +7 -0
- package/dist/api/auth/account/reset-password.d.ts.map +1 -0
- package/dist/api/auth/account/reset-password.js +36 -0
- package/dist/api/auth/check-username.d.ts +9 -0
- package/dist/api/auth/check-username.d.ts.map +1 -0
- package/dist/api/auth/check-username.js +35 -0
- package/dist/api/auth/establish-session.d.ts +2 -0
- package/dist/api/auth/establish-session.d.ts.map +1 -0
- package/dist/api/auth/establish-session.js +23 -0
- package/dist/api/auth/me.d.ts +4 -4
- package/dist/api/auth/me.d.ts.map +1 -1
- package/dist/api/auth/me.js +28 -6
- package/dist/api/auth/profile.d.ts.map +1 -1
- package/dist/api/auth/profile.js +6 -3
- package/dist/api/auth/storage/recalculate.d.ts +15 -0
- package/dist/api/auth/storage/recalculate.d.ts.map +1 -0
- package/dist/api/auth/storage/recalculate.js +68 -0
- package/dist/api/auth/storage/usage.d.ts +10 -0
- package/dist/api/auth/storage/usage.d.ts.map +1 -0
- package/dist/api/auth/storage/usage.js +86 -0
- package/dist/api/public/add-welcome-bonus.d.ts +16 -0
- package/dist/api/public/add-welcome-bonus.d.ts.map +1 -0
- package/dist/api/public/add-welcome-bonus.js +177 -0
- package/dist/api/public/callback.d.ts +3 -0
- package/dist/api/public/callback.d.ts.map +1 -0
- package/dist/api/public/callback.js +197 -0
- package/dist/api/public/reset-password.d.ts +3 -0
- package/dist/api/public/reset-password.d.ts.map +1 -0
- package/dist/api/public/reset-password.js +43 -0
- package/dist/api/public/set-session.d.ts +7 -0
- package/dist/api/public/set-session.d.ts.map +1 -0
- package/dist/api/public/set-session.js +55 -0
- package/dist/api/public/signin.d.ts.map +1 -1
- package/dist/api/public/signin.js +31 -0
- package/dist/api/public/signup.d.ts.map +1 -1
- package/dist/api/public/signup.js +38 -27
- package/dist/api/public/webhook/storage-addon.d.ts +9 -0
- package/dist/api/public/webhook/storage-addon.d.ts.map +1 -0
- package/dist/api/public/webhook/storage-addon.js +155 -0
- package/dist/api/storage.js +2 -2
- package/dist/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +134 -14
- package/dist/components/AccountButton.d.ts.map +1 -1
- package/dist/components/AccountButton.js +54 -28
- package/dist/components/Doc.d.ts.map +1 -1
- package/dist/components/Doc.js +1 -1
- package/dist/components/HasProfil.d.ts +4 -0
- package/dist/components/HasProfil.d.ts.map +1 -0
- package/dist/components/HasProfil.js +39 -0
- package/dist/{web → components}/auth/dashboard.d.ts +1 -1
- package/dist/components/auth/dashboard.d.ts.map +1 -0
- package/dist/components/auth/dashboard.js +74 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lib/app-branding-data.d.ts +22 -0
- package/dist/lib/app-branding-data.d.ts.map +1 -0
- package/dist/lib/app-branding-data.js +49 -0
- package/dist/lib/auth-email-service.d.ts +57 -0
- package/dist/lib/auth-email-service.d.ts.map +1 -0
- package/dist/lib/auth-email-service.js +382 -0
- package/dist/lib/auth-email-templates.d.ts +2 -0
- package/dist/lib/auth-email-templates.d.ts.map +1 -0
- package/dist/lib/auth-email-templates.js +1 -0
- package/dist/lib/site-url.d.ts +3 -0
- package/dist/lib/site-url.d.ts.map +1 -0
- package/dist/lib/site-url.js +11 -0
- package/dist/sitemap/manifest.d.ts +9 -0
- package/dist/sitemap/manifest.d.ts.map +1 -0
- package/dist/sitemap/manifest.js +14 -0
- package/dist/web/admin/signup-stats.js +3 -3
- package/dist/web/admin/user-detail.d.ts.map +1 -1
- package/dist/web/admin/user-detail.js +135 -14
- package/dist/web/admin/users-by-signup-source.js +2 -2
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +26 -7
- package/dist/web/auth/folder.d.ts.map +1 -1
- package/dist/web/auth/folder.js +4 -3
- package/dist/web/auth/profile.d.ts.map +1 -1
- package/dist/web/auth/profile.js +132 -13
- package/dist/web/auth/reglage.d.ts.map +1 -1
- package/dist/web/auth/reglage.js +15 -8
- package/dist/web/public/ResetPassword.d.ts.map +1 -1
- package/dist/web/public/ResetPassword.js +172 -2
- package/dist/web/public/SignInPage.d.ts.map +1 -1
- package/dist/web/public/SignInPage.js +39 -3
- package/dist/web/public/SignUpPage.d.ts.map +1 -1
- package/dist/web/public/SignUpPage.js +7 -2
- package/dist/web/public/auth-code-error.d.ts +2 -0
- package/dist/web/public/auth-code-error.d.ts.map +1 -0
- package/dist/web/public/auth-code-error.js +14 -0
- package/dist/web/public/confirm.d.ts +2 -0
- package/dist/web/public/confirm.d.ts.map +1 -0
- package/dist/web/public/confirm.js +157 -0
- package/package.json +10 -5
- package/src/api/admin/signup-stats.ts +2 -1
- package/src/api/admin/storage/usage.ts +141 -0
- package/src/api/admin/users/[id]/notifications.ts +3 -2
- package/src/api/admin/users/[id].ts +3 -2
- package/src/api/admin/users/reactivate/[id].ts +88 -0
- package/src/api/admin/users/suspend/[id].ts +85 -0
- package/src/api/admin/users-by-source.ts +2 -1
- package/src/api/admin/users.ts +59 -2
- package/src/api/auth/account/email-change.ts +54 -0
- package/src/api/auth/account/reset-password.ts +47 -0
- package/src/api/auth/check-username.ts +52 -0
- package/src/api/auth/establish-session.ts +32 -0
- package/src/api/auth/me.ts +29 -7
- package/src/api/auth/profile.ts +6 -2
- package/src/api/auth/storage/recalculate.ts +108 -0
- package/src/api/auth/storage/usage.ts +113 -0
- package/src/api/public/add-welcome-bonus.ts +229 -0
- package/src/api/public/callback.ts +307 -0
- package/src/api/public/reset-password.ts +52 -0
- package/src/api/public/set-session.ts +73 -0
- package/src/api/public/signin.ts +36 -0
- package/src/api/public/signup.ts +44 -37
- package/src/api/public/webhook/storage-addon.ts +267 -0
- package/src/api/storage.ts +2 -2
- package/src/auth.build.config.ts +134 -14
- package/src/components/AccountButton.tsx +114 -90
- package/src/components/Doc.tsx +47 -9
- package/src/components/HasProfil.tsx +63 -0
- package/src/{web → components}/auth/dashboard.tsx +64 -20
- package/src/i18n/en.json +78 -8
- package/src/i18n/es.json +330 -0
- package/src/i18n/fr.json +75 -8
- package/src/index.ts +3 -1
- package/src/lib/app-branding-data.ts +90 -0
- package/src/lib/auth-email-service.ts +508 -0
- package/src/lib/auth-email-templates.ts +5 -0
- package/src/lib/site-url.ts +17 -0
- package/src/sitemap/manifest.ts +26 -0
- package/src/web/admin/signup-stats.tsx +3 -3
- package/src/web/admin/user-detail.tsx +314 -15
- package/src/web/admin/users-by-signup-source.tsx +2 -2
- package/src/web/admin/users.tsx +50 -14
- package/src/web/auth/folder.tsx +23 -5
- package/src/web/auth/profile.tsx +227 -13
- package/src/web/auth/reglage.tsx +55 -24
- package/src/web/public/ResetPassword.tsx +301 -1
- package/src/web/public/SignInPage.tsx +43 -3
- package/src/web/public/SignUpPage.tsx +14 -5
- package/src/web/public/auth-code-error.tsx +49 -0
- package/src/web/public/confirm.tsx +195 -0
- package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +3 -1
- package/supabase/migrations/20251127100000_rename_body_to_message.sql +8 -3
- package/supabase/migrations/20260120150001_add_user_storage.sql +105 -0
- package/supabase/migrations/20260122131200_add_global_addons_system.sql +305 -0
- package/supabase/migrations/20260123100000_enable_vector_extension.sql +9 -0
- package/supabase/migrations/20260123140000_module_auth_fix_get_user_limits_null.sql +93 -0
- package/supabase/migrations/20260123145000_module_auth_fix_circular_storage_base.sql +89 -0
- package/supabase/migrations/20260129400000_add_username_to_user_profil.sql +33 -0
- package/dist/web/auth/dashboard.d.ts.map +0 -1
- 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
|
-
|
|
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
|
|
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="/
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|