@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.
- 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 +126 -11
- 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/components/auth/dashboard.d.ts +1 -1
- package/dist/components/auth/dashboard.d.ts.map +1 -1
- package/dist/components/auth/dashboard.js +34 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- 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 +126 -11
- package/src/components/AccountButton.tsx +114 -90
- package/src/components/Doc.tsx +47 -9
- package/src/components/HasProfil.tsx +63 -0
- package/src/components/auth/dashboard.tsx +54 -13
- package/src/i18n/en.json +76 -8
- package/src/i18n/es.json +330 -0
- package/src/i18n/fr.json +74 -8
- package/src/index.ts +2 -0
- 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 +0 -2
- package/dist/web/auth/dashboard.d.ts.map +0 -1
- package/dist/web/auth/dashboard.js +0 -48
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSupabaseServiceClient,
|
|
3
|
+
getSupabaseServerClient,
|
|
4
|
+
} from "@lastbrain/core/server";
|
|
5
|
+
import {
|
|
6
|
+
sendEmail,
|
|
7
|
+
type AuthEmailKind,
|
|
8
|
+
getAuthEmailSubject,
|
|
9
|
+
renderAuthEmailHtml,
|
|
10
|
+
} from "@lastbrain-labs/module-contact-pro";
|
|
11
|
+
import { getAppBranding } from "./app-branding-data";
|
|
12
|
+
import { logger } from "@lastbrain/core";
|
|
13
|
+
import { cookies } from "next/headers";
|
|
14
|
+
|
|
15
|
+
const FALLBACK_SITE_URL =
|
|
16
|
+
process.env.NEXT_PUBLIC_SITE_URL ||
|
|
17
|
+
process.env.SITE_URL ||
|
|
18
|
+
"http://localhost:3000";
|
|
19
|
+
const NORMALIZED_SITE_URL = FALLBACK_SITE_URL.replace(/\/$/, "");
|
|
20
|
+
const DEFAULT_LOCALE = "fr";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Récupère la langue depuis le cookie NEXT_LOCALE
|
|
24
|
+
*/
|
|
25
|
+
async function getLocaleFromCookie(): Promise<string | undefined> {
|
|
26
|
+
try {
|
|
27
|
+
const cookieStore = await cookies();
|
|
28
|
+
const localeCookie = cookieStore.get("NEXT_LOCALE");
|
|
29
|
+
if (localeCookie?.value && /^[a-z]{2}$/.test(localeCookie.value)) {
|
|
30
|
+
return localeCookie.value;
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.debug("[getLocaleFromCookie] Could not read cookie:", error);
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface BaseEmailParams {
|
|
39
|
+
email: string;
|
|
40
|
+
displayName?: string;
|
|
41
|
+
ownerId?: string;
|
|
42
|
+
redirectTo?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function generateLink(
|
|
46
|
+
type:
|
|
47
|
+
| "signup"
|
|
48
|
+
| "invite"
|
|
49
|
+
| "magiclink"
|
|
50
|
+
| "recovery"
|
|
51
|
+
| "email_change"
|
|
52
|
+
| "reauthentication",
|
|
53
|
+
params: any
|
|
54
|
+
) {
|
|
55
|
+
const supabase = await getSupabaseServiceClient();
|
|
56
|
+
return supabase.auth.admin.generateLink({ type, ...params });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function sendAuthEmail(
|
|
60
|
+
kind: AuthEmailKind,
|
|
61
|
+
to: string,
|
|
62
|
+
actionLink: string,
|
|
63
|
+
options: { displayName?: string; ownerId?: string; locale?: string }
|
|
64
|
+
) {
|
|
65
|
+
const branding = getAppBranding(options.locale);
|
|
66
|
+
logger.debug("📧 sendAuthEmail - branding:", {
|
|
67
|
+
locale: options.locale,
|
|
68
|
+
appName: branding.appName,
|
|
69
|
+
icon: branding.icon,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const html = renderAuthEmailHtml(kind, {
|
|
73
|
+
kind,
|
|
74
|
+
actionLink,
|
|
75
|
+
displayName: options.displayName,
|
|
76
|
+
locale: options.locale,
|
|
77
|
+
appName: branding.appName,
|
|
78
|
+
appIcon: branding.icon,
|
|
79
|
+
appTagline: branding.tagline,
|
|
80
|
+
supportEmail: branding.supportEmail,
|
|
81
|
+
primaryColor: branding.primaryColor,
|
|
82
|
+
secondaryColor: branding.secondaryColor,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = await sendEmail({
|
|
87
|
+
to,
|
|
88
|
+
subject: getAuthEmailSubject(kind, options.locale),
|
|
89
|
+
html,
|
|
90
|
+
emailType: "transactional",
|
|
91
|
+
ownerId: options.ownerId,
|
|
92
|
+
});
|
|
93
|
+
logger.debug("📧 sendEmail result", { ok: true, result });
|
|
94
|
+
return result;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
logger.error("📧 sendEmail error", err);
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function sendSignupConfirmationEmail(params: {
|
|
102
|
+
email: string;
|
|
103
|
+
password: string;
|
|
104
|
+
fullName?: string;
|
|
105
|
+
signupSource?: string;
|
|
106
|
+
redirectTo?: string;
|
|
107
|
+
next?: string;
|
|
108
|
+
locale?: string;
|
|
109
|
+
}) {
|
|
110
|
+
// Récupérer la langue depuis le cookie si non fournie
|
|
111
|
+
const locale =
|
|
112
|
+
params.locale || (await getLocaleFromCookie()) || DEFAULT_LOCALE;
|
|
113
|
+
|
|
114
|
+
// Use magic link for signup so client receives tokens in URL fragment
|
|
115
|
+
// Inclure la locale dans l'URL si pas déjà présente
|
|
116
|
+
let redirectTo =
|
|
117
|
+
params.redirectTo || `${NORMALIZED_SITE_URL}/${locale}/confirm`;
|
|
118
|
+
|
|
119
|
+
// Si un redirectTo personnalisé est fourni sans locale, l'ajouter
|
|
120
|
+
if (params.redirectTo && !params.redirectTo.match(/\/(fr|en|es|de)\//)) {
|
|
121
|
+
const url = new URL(params.redirectTo, NORMALIZED_SITE_URL);
|
|
122
|
+
url.pathname = `/${locale}${url.pathname}`;
|
|
123
|
+
redirectTo = url.toString();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (params.next) {
|
|
127
|
+
const sep = redirectTo.includes("?") ? "&" : "?";
|
|
128
|
+
redirectTo = `${redirectTo}${sep}next=${encodeURIComponent(params.next)}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { data, error } = await generateLink("magiclink", {
|
|
132
|
+
email: params.email,
|
|
133
|
+
options: {
|
|
134
|
+
redirectTo,
|
|
135
|
+
data: {
|
|
136
|
+
full_name: params.fullName,
|
|
137
|
+
signup_source: params.signupSource,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (error) throw error;
|
|
143
|
+
|
|
144
|
+
const actionLink =
|
|
145
|
+
(data as any)?.properties?.action_link || (data as any)?.action_link;
|
|
146
|
+
|
|
147
|
+
await sendAuthEmail("magic_link", params.email, actionLink, {
|
|
148
|
+
displayName: params.fullName,
|
|
149
|
+
ownerId: data.user?.id,
|
|
150
|
+
locale,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return data.user;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function sendPasswordResetEmail(
|
|
157
|
+
params: BaseEmailParams & { locale?: string; next?: string }
|
|
158
|
+
) {
|
|
159
|
+
// Récupérer la langue depuis le cookie si non fournie
|
|
160
|
+
const cookieLocale = await getLocaleFromCookie();
|
|
161
|
+
|
|
162
|
+
logger.debug("[sendPasswordResetEmail] locale detection", {
|
|
163
|
+
providedLocale: params.locale,
|
|
164
|
+
cookieLocale,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Attempt to infer locale from redirectTo path or use provided locale
|
|
168
|
+
const inferredLocale = (() => {
|
|
169
|
+
// Priority: 1. provided locale, 2. cookie, 3. redirectTo path
|
|
170
|
+
if (params.locale && (params.locale === "fr" || params.locale === "en")) {
|
|
171
|
+
return params.locale;
|
|
172
|
+
}
|
|
173
|
+
if (cookieLocale) {
|
|
174
|
+
return cookieLocale;
|
|
175
|
+
}
|
|
176
|
+
// Otherwise, try to infer from redirectTo URL path
|
|
177
|
+
const url = params.redirectTo || `${FALLBACK_SITE_URL}/api/auth/callback`;
|
|
178
|
+
try {
|
|
179
|
+
const u = new URL(url);
|
|
180
|
+
const seg = u.pathname.split("/").filter(Boolean)[0];
|
|
181
|
+
return seg === "fr" || seg === "en" ? seg : undefined;
|
|
182
|
+
} catch {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
|
|
187
|
+
// Default to the reset-password page so the client can handle tokens directly.
|
|
188
|
+
const targetLocale = inferredLocale || DEFAULT_LOCALE;
|
|
189
|
+
|
|
190
|
+
logger.debug("[sendPasswordResetEmail] final locale", {
|
|
191
|
+
inferredLocale,
|
|
192
|
+
targetLocale,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Construire le redirectTo avec la locale
|
|
196
|
+
let redirectTo =
|
|
197
|
+
params.redirectTo ||
|
|
198
|
+
`${NORMALIZED_SITE_URL}/${targetLocale}/reset-password`;
|
|
199
|
+
|
|
200
|
+
// Si un redirectTo personnalisé est fourni sans locale, l'ajouter
|
|
201
|
+
if (params.redirectTo && !params.redirectTo.match(/\/(fr|en|es|de)\//)) {
|
|
202
|
+
const url = new URL(params.redirectTo, NORMALIZED_SITE_URL);
|
|
203
|
+
url.pathname = `/${targetLocale}${url.pathname}`;
|
|
204
|
+
redirectTo = url.toString();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if ((params as any).next) {
|
|
208
|
+
const next = (params as any).next as string;
|
|
209
|
+
const sep = redirectTo.includes("?") ? "&" : "?";
|
|
210
|
+
redirectTo = `${redirectTo}${sep}next=${encodeURIComponent(next)}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
logger.debug(
|
|
214
|
+
"[sendPasswordResetEmail] calling generateLink with redirectTo",
|
|
215
|
+
{
|
|
216
|
+
redirectTo,
|
|
217
|
+
email: params.email,
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const { data, error } = await generateLink("recovery", {
|
|
222
|
+
email: params.email,
|
|
223
|
+
options: { redirectTo },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
logger.debug("[sendPasswordResetEmail] generateLink returned", {
|
|
227
|
+
action_link:
|
|
228
|
+
(data as any)?.properties?.action_link || (data as any)?.action_link,
|
|
229
|
+
supabaseData: data,
|
|
230
|
+
error,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (error) throw error;
|
|
234
|
+
|
|
235
|
+
const actionLink =
|
|
236
|
+
(data as any)?.properties?.action_link || (data as any)?.action_link;
|
|
237
|
+
|
|
238
|
+
await sendAuthEmail("magic_link", params.email, actionLink, {
|
|
239
|
+
displayName: params.displayName,
|
|
240
|
+
ownerId: params.ownerId,
|
|
241
|
+
locale: targetLocale,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return data.user;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function sendMagicLinkEmail(
|
|
248
|
+
params: BaseEmailParams & { locale?: string }
|
|
249
|
+
) {
|
|
250
|
+
// Récupérer la langue depuis le cookie si non fournie
|
|
251
|
+
const cookieLocale = await getLocaleFromCookie();
|
|
252
|
+
|
|
253
|
+
const inferredLocale = (() => {
|
|
254
|
+
if (params.locale && (params.locale === "fr" || params.locale === "en")) {
|
|
255
|
+
return params.locale;
|
|
256
|
+
}
|
|
257
|
+
if (cookieLocale) {
|
|
258
|
+
return cookieLocale;
|
|
259
|
+
}
|
|
260
|
+
const url = params.redirectTo || `${FALLBACK_SITE_URL}/api/auth/callback`;
|
|
261
|
+
try {
|
|
262
|
+
const u = new URL(url);
|
|
263
|
+
const seg = u.pathname.split("/").filter(Boolean)[0];
|
|
264
|
+
return seg === "fr" || seg === "en" ? seg : undefined;
|
|
265
|
+
} catch {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
269
|
+
|
|
270
|
+
const redirectTo =
|
|
271
|
+
params.redirectTo || `${NORMALIZED_SITE_URL}/api/public/callback`;
|
|
272
|
+
const { data, error } = await generateLink("magiclink", {
|
|
273
|
+
email: params.email,
|
|
274
|
+
options: { redirectTo },
|
|
275
|
+
});
|
|
276
|
+
if (error) throw error;
|
|
277
|
+
const actionLink =
|
|
278
|
+
(data as any)?.properties?.action_link || (data as any)?.action_link;
|
|
279
|
+
await sendAuthEmail("magic_link", params.email, actionLink, {
|
|
280
|
+
displayName: params.displayName,
|
|
281
|
+
ownerId: params.ownerId,
|
|
282
|
+
locale: inferredLocale,
|
|
283
|
+
});
|
|
284
|
+
return data.user;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function sendInviteEmail(
|
|
288
|
+
params: BaseEmailParams & { password?: string; locale?: string }
|
|
289
|
+
) {
|
|
290
|
+
const inferredLocale = (() => {
|
|
291
|
+
if (params.locale && (params.locale === "fr" || params.locale === "en")) {
|
|
292
|
+
return params.locale;
|
|
293
|
+
}
|
|
294
|
+
const url = params.redirectTo || `${FALLBACK_SITE_URL}/api/auth/callback`;
|
|
295
|
+
try {
|
|
296
|
+
const u = new URL(url);
|
|
297
|
+
const seg = u.pathname.split("/").filter(Boolean)[0];
|
|
298
|
+
return seg === "fr" || seg === "en" ? seg : undefined;
|
|
299
|
+
} catch {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
})();
|
|
303
|
+
|
|
304
|
+
const redirectTo =
|
|
305
|
+
params.redirectTo || `${NORMALIZED_SITE_URL}/api/public/callback`;
|
|
306
|
+
const { data, error } = await generateLink("invite", {
|
|
307
|
+
email: params.email,
|
|
308
|
+
password: params.password,
|
|
309
|
+
options: { redirectTo },
|
|
310
|
+
});
|
|
311
|
+
if (error) throw error;
|
|
312
|
+
const actionLink =
|
|
313
|
+
(data as any)?.properties?.action_link || (data as any)?.action_link;
|
|
314
|
+
await sendAuthEmail("invite", params.email, actionLink, {
|
|
315
|
+
displayName: params.displayName,
|
|
316
|
+
ownerId: params.ownerId ?? data.user?.id,
|
|
317
|
+
locale: inferredLocale,
|
|
318
|
+
});
|
|
319
|
+
return data.user;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function sendEmailChangeConfirmation(params: {
|
|
323
|
+
email: string; // current email
|
|
324
|
+
newEmail: string;
|
|
325
|
+
displayName?: string;
|
|
326
|
+
ownerId?: string;
|
|
327
|
+
redirectTo?: string;
|
|
328
|
+
locale?: string;
|
|
329
|
+
}) {
|
|
330
|
+
const redirectTo =
|
|
331
|
+
params.redirectTo || `${NORMALIZED_SITE_URL}/api/auth/callback`;
|
|
332
|
+
const { data, error } = await generateLink("email_change", {
|
|
333
|
+
email: params.email,
|
|
334
|
+
newEmail: params.newEmail,
|
|
335
|
+
options: { redirectTo },
|
|
336
|
+
});
|
|
337
|
+
if (error) throw error;
|
|
338
|
+
const actionLink =
|
|
339
|
+
(data as any)?.properties?.action_link || (data as any)?.action_link;
|
|
340
|
+
await sendAuthEmail("confirm_email_change", params.newEmail, actionLink, {
|
|
341
|
+
displayName: params.displayName,
|
|
342
|
+
ownerId: params.ownerId,
|
|
343
|
+
locale: params.locale,
|
|
344
|
+
});
|
|
345
|
+
return data.user;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function sendReauthenticationEmail(
|
|
349
|
+
params: BaseEmailParams & { locale?: string }
|
|
350
|
+
) {
|
|
351
|
+
const inferredLocale = (() => {
|
|
352
|
+
if (params.locale && (params.locale === "fr" || params.locale === "en")) {
|
|
353
|
+
return params.locale;
|
|
354
|
+
}
|
|
355
|
+
const url = params.redirectTo || `${FALLBACK_SITE_URL}/api/auth/callback`;
|
|
356
|
+
try {
|
|
357
|
+
const u = new URL(url);
|
|
358
|
+
const seg = u.pathname.split("/").filter(Boolean)[0];
|
|
359
|
+
return seg === "fr" || seg === "en" ? seg : undefined;
|
|
360
|
+
} catch {
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
})();
|
|
364
|
+
|
|
365
|
+
const redirectTo =
|
|
366
|
+
params.redirectTo || `${NORMALIZED_SITE_URL}/api/auth/callback`;
|
|
367
|
+
const { data, error } = await generateLink("reauthentication", {
|
|
368
|
+
email: params.email,
|
|
369
|
+
options: { redirectTo },
|
|
370
|
+
});
|
|
371
|
+
if (error) throw error;
|
|
372
|
+
const actionLink =
|
|
373
|
+
(data as any)?.properties?.action_link || (data as any)?.action_link;
|
|
374
|
+
await sendAuthEmail("reauthentication", params.email, actionLink, {
|
|
375
|
+
displayName: params.displayName,
|
|
376
|
+
ownerId: params.ownerId,
|
|
377
|
+
locale: inferredLocale,
|
|
378
|
+
});
|
|
379
|
+
return data.user;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function fetchCurrentUserOwnerId(): Promise<string | null> {
|
|
383
|
+
const supabase = await getSupabaseServerClient();
|
|
384
|
+
const {
|
|
385
|
+
data: { user },
|
|
386
|
+
} = await supabase.auth.getUser();
|
|
387
|
+
return user?.id || null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function sendWelcomeOnboardingEmail(params: {
|
|
391
|
+
email: string;
|
|
392
|
+
displayName?: string;
|
|
393
|
+
locale?: string;
|
|
394
|
+
ownerId?: string;
|
|
395
|
+
bonusAmountUsd?: number;
|
|
396
|
+
}) {
|
|
397
|
+
const branding = getAppBranding(params.locale);
|
|
398
|
+
|
|
399
|
+
// Render the welcome onboarding email using the template from module-contact-pro
|
|
400
|
+
const html = renderAuthEmailHtml("welcome_onboarding", {
|
|
401
|
+
kind: "welcome_onboarding",
|
|
402
|
+
actionLink: `${FALLBACK_SITE_URL}/`,
|
|
403
|
+
displayName: params.displayName,
|
|
404
|
+
locale: params.locale,
|
|
405
|
+
appName: branding.appName,
|
|
406
|
+
appIcon: branding.icon,
|
|
407
|
+
appTagline: branding.tagline,
|
|
408
|
+
supportEmail: branding.supportEmail,
|
|
409
|
+
bonusAmountUsd: params.bonusAmountUsd,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
await sendEmail({
|
|
413
|
+
to: params.email,
|
|
414
|
+
subject: getAuthEmailSubject(
|
|
415
|
+
"welcome_onboarding",
|
|
416
|
+
params.locale,
|
|
417
|
+
params.bonusAmountUsd
|
|
418
|
+
),
|
|
419
|
+
html,
|
|
420
|
+
emailType: "transactional",
|
|
421
|
+
ownerId: params.ownerId,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Send notification to admin when new user signs up
|
|
429
|
+
*/
|
|
430
|
+
export async function sendNewUserNotificationToAdmin(params: {
|
|
431
|
+
userEmail: string;
|
|
432
|
+
displayName?: string;
|
|
433
|
+
userId: string;
|
|
434
|
+
locale?: string;
|
|
435
|
+
bonusAmountUsd?: number;
|
|
436
|
+
}) {
|
|
437
|
+
const ADMIN_EMAIL = "contact@lastbrain.io";
|
|
438
|
+
const branding = getAppBranding("fr");
|
|
439
|
+
|
|
440
|
+
const html = `
|
|
441
|
+
<!DOCTYPE html>
|
|
442
|
+
<html>
|
|
443
|
+
<head>
|
|
444
|
+
<meta charset="utf-8">
|
|
445
|
+
<style>
|
|
446
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
447
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
448
|
+
.header { background: #4F46E5; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
449
|
+
.content { background: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }
|
|
450
|
+
.info-row { margin: 10px 0; padding: 10px; background: white; border-radius: 4px; }
|
|
451
|
+
.label { font-weight: 600; color: #6B7280; }
|
|
452
|
+
.value { color: #111827; }
|
|
453
|
+
</style>
|
|
454
|
+
</head>
|
|
455
|
+
<body>
|
|
456
|
+
<div class="container">
|
|
457
|
+
<div class="header">
|
|
458
|
+
<h2>🎉 Nouvelle inscription - ${branding.appName}</h2>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="content">
|
|
461
|
+
<p>Un nouvel utilisateur vient de s'inscrire sur la plateforme :</p>
|
|
462
|
+
|
|
463
|
+
<div class="info-row">
|
|
464
|
+
<span class="label">Email :</span>
|
|
465
|
+
<span class="value">${params.userEmail}</span>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
${params.displayName ? `<div class="info-row"><span class="label">Nom :</span><span class="value">${params.displayName}</span></div>` : ""}
|
|
469
|
+
|
|
470
|
+
<div class="info-row">
|
|
471
|
+
<span class="label">User ID :</span>
|
|
472
|
+
<span class="value">${params.userId}</span>
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<div class="info-row">
|
|
476
|
+
<span class="label">Langue :</span>
|
|
477
|
+
<span class="value">${params.locale || "non définie"}</span>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
${params.bonusAmountUsd ? `<div class="info-row"><span class="label">Bonus accordé :</span><span class="value">$${params.bonusAmountUsd}</span></div>` : ""}
|
|
481
|
+
|
|
482
|
+
<div class="info-row">
|
|
483
|
+
<span class="label">Date :</span>
|
|
484
|
+
<span class="value">${new Date().toLocaleString("fr-FR")}</span>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</body>
|
|
489
|
+
</html>
|
|
490
|
+
`;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
await sendEmail({
|
|
494
|
+
to: ADMIN_EMAIL,
|
|
495
|
+
subject: `[${branding.appName}] Nouvelle inscription : ${params.userEmail}`,
|
|
496
|
+
html,
|
|
497
|
+
emailType: "transactional",
|
|
498
|
+
});
|
|
499
|
+
logger.info(
|
|
500
|
+
`[sendNewUserNotificationToAdmin] Notification sent to ${ADMIN_EMAIL} for user ${params.userId}`
|
|
501
|
+
);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
logger.error(
|
|
504
|
+
"[sendNewUserNotificationToAdmin] Failed to send admin notification:",
|
|
505
|
+
error
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
|
|
3
|
+
const FALLBACK_SITE_URL =
|
|
4
|
+
process.env.NEXT_PUBLIC_SITE_URL ||
|
|
5
|
+
process.env.SITE_URL ||
|
|
6
|
+
"http://localhost:3000";
|
|
7
|
+
|
|
8
|
+
export function resolveSiteUrl(request: NextRequest) {
|
|
9
|
+
const origin =
|
|
10
|
+
process.env.NEXT_PUBLIC_SITE_URL ||
|
|
11
|
+
process.env.SITE_URL ||
|
|
12
|
+
request.headers.get("origin") ||
|
|
13
|
+
request.nextUrl.origin ||
|
|
14
|
+
FALLBACK_SITE_URL;
|
|
15
|
+
|
|
16
|
+
return origin.replace(/\/$/, "");
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest des sitemaps du module auth
|
|
3
|
+
* Déclare tous les sitemaps enfants à générer
|
|
4
|
+
* Auth module only has static pages (signin, signup, etc.)
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
SitemapManifest,
|
|
8
|
+
SitemapChild,
|
|
9
|
+
SitemapChildKind,
|
|
10
|
+
} from "@lastbrain/core";
|
|
11
|
+
|
|
12
|
+
export type { SitemapManifest, SitemapChild, SitemapChildKind };
|
|
13
|
+
export const sitemapManifest: SitemapManifest = {
|
|
14
|
+
module: "auth",
|
|
15
|
+
enabled: true,
|
|
16
|
+
includePublicPagesFromBuildConfig: true,
|
|
17
|
+
children: [
|
|
18
|
+
// Pages statiques (générées auto depuis build.config)
|
|
19
|
+
// Includes: /signin, /signup, /reset-password, /confirm, etc.
|
|
20
|
+
{
|
|
21
|
+
id: "static",
|
|
22
|
+
path: ":lang/static.xml",
|
|
23
|
+
kind: "static",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
} as const;
|
|
@@ -65,7 +65,7 @@ export function SignupStatsPage() {
|
|
|
65
65
|
|
|
66
66
|
if (loading) {
|
|
67
67
|
return (
|
|
68
|
-
<div className="flex justify-center items-center min-h-screen">
|
|
68
|
+
<div className="mt-16 flex justify-center items-center min-h-screen">
|
|
69
69
|
<Spinner
|
|
70
70
|
size="lg"
|
|
71
71
|
label={t("signup_stats.loading") || "Chargement des statistiques..."}
|
|
@@ -76,7 +76,7 @@ export function SignupStatsPage() {
|
|
|
76
76
|
|
|
77
77
|
if (error || !stats) {
|
|
78
78
|
return (
|
|
79
|
-
<div className="p-6">
|
|
79
|
+
<div className="mt-16 p-6">
|
|
80
80
|
<Card className="border border-danger-200 bg-danger-50/50">
|
|
81
81
|
<CardBody>
|
|
82
82
|
<p className="text-danger-600">{error || "Erreur de chargement"}</p>
|
|
@@ -96,7 +96,7 @@ export function SignupStatsPage() {
|
|
|
96
96
|
).toFixed(1);
|
|
97
97
|
|
|
98
98
|
return (
|
|
99
|
-
<div className="space-y-6 px-2 md:p-6">
|
|
99
|
+
<div className="mt-16 space-y-6 px-2 md:p-6">
|
|
100
100
|
{/* Header */}
|
|
101
101
|
<div className="flex items-center gap-2 mb-8">
|
|
102
102
|
<BarChart3 size={28} className="text-primary-600" />
|