@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.
Files changed (178) 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 +134 -14
  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/{web → components}/auth/dashboard.d.ts +1 -1
  74. package/dist/components/auth/dashboard.d.ts.map +1 -0
  75. package/dist/components/auth/dashboard.js +74 -0
  76. package/dist/index.d.ts +3 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +3 -1
  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 +134 -14
  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/{web → components}/auth/dashboard.tsx +64 -20
  148. package/src/i18n/en.json +78 -8
  149. package/src/i18n/es.json +330 -0
  150. package/src/i18n/fr.json +75 -8
  151. package/src/index.ts +3 -1
  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.map +0 -1
  178. package/dist/web/auth/dashboard.js +0 -48
@@ -0,0 +1,90 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ /**
5
+ * Branding configuration for emails
6
+ * Loaded dynamically from app i18n files (app.name, app.icon, etc.)
7
+ */
8
+
9
+ export interface AppBrandingData {
10
+ appName: string;
11
+ logo?: string;
12
+ description?: string;
13
+ tagline?: string;
14
+ icon?: string;
15
+ twitterCreator?: string;
16
+ twitterSite?: string;
17
+ supportEmail?: string;
18
+ primaryColor?: string;
19
+ secondaryColor?: string;
20
+ }
21
+
22
+ /**
23
+ * Charge les traductions de l'app depuis les fichiers i18n agrégés
24
+ */
25
+ function loadAppTranslations(lang: string): Record<string, string> {
26
+ try {
27
+ const possiblePaths = [
28
+ // Production (Vercel)
29
+ path.join("/app", "i18n", `${lang}.json`),
30
+ // Development - apps/lastbrain
31
+ path.join(process.cwd(), "i18n", `${lang}.json`),
32
+ // Development - depuis packages vers apps/lastbrain
33
+ path.join(
34
+ process.cwd(),
35
+ "..",
36
+ "..",
37
+ "apps",
38
+ "lastbrain",
39
+ "i18n",
40
+ `${lang}.json`
41
+ ),
42
+ // Development - apps/recipe
43
+ path.join(
44
+ process.cwd(),
45
+ "..",
46
+ "..",
47
+ "apps",
48
+ "recipe",
49
+ "i18n",
50
+ `${lang}.json`
51
+ ),
52
+ ];
53
+
54
+ for (const filePath of possiblePaths) {
55
+ if (fs.existsSync(filePath)) {
56
+ const content = fs.readFileSync(filePath, "utf-8");
57
+ return JSON.parse(content);
58
+ }
59
+ }
60
+ } catch (error) {
61
+ console.warn(
62
+ `[getAppBranding] Could not load translations for ${lang}:`,
63
+ error
64
+ );
65
+ }
66
+
67
+ return {};
68
+ }
69
+
70
+ /**
71
+ * Récupère le branding depuis les clés i18n de l'app
72
+ * Utilise les clés : app.name, app.icon, app.description, app.tagline, etc.
73
+ */
74
+ export function getAppBranding(locale: string = "en"): AppBrandingData {
75
+ const loc = locale === "fr" ? "fr" : "en";
76
+ const translations = loadAppTranslations(loc);
77
+
78
+ return {
79
+ appName: translations["app.name"] || "LastBrain",
80
+ icon: translations["app.icon"] || "Brain",
81
+ logo: translations["app.icon"] || "Brain",
82
+ description: translations["app.description"] || "",
83
+ tagline: translations["app.tagline"] || "",
84
+ twitterCreator: translations["app.twitter_creator"] || "",
85
+ twitterSite: translations["app.twitter_site"] || "",
86
+ supportEmail: translations["app.email.contact"] || "support@lastbrain.io",
87
+ primaryColor: translations["app.email.color.primary"] || "#3b82f6",
88
+ secondaryColor: translations["app.email.color.secondary"] || "#8b5cf6",
89
+ };
90
+ }
@@ -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,5 @@
1
+ export {
2
+ type AuthEmailKind,
3
+ getAuthEmailSubject,
4
+ renderAuthEmailHtml,
5
+ } from "@lastbrain-labs/module-contact-pro";
@@ -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" />