@lastbrain/module-auth 2.0.27 → 2.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +55 -7
  2. package/dist/api/admin/signup-stats.d.ts.map +1 -1
  3. package/dist/api/admin/signup-stats.js +2 -1
  4. package/dist/api/admin/storage/usage.d.ts +18 -0
  5. package/dist/api/admin/storage/usage.d.ts.map +1 -0
  6. package/dist/api/admin/storage/usage.js +100 -0
  7. package/dist/api/admin/users/[id]/notifications.d.ts.map +1 -1
  8. package/dist/api/admin/users/[id]/notifications.js +3 -2
  9. package/dist/api/admin/users/[id].d.ts.map +1 -1
  10. package/dist/api/admin/users/[id].js +3 -2
  11. package/dist/api/admin/users/reactivate/[id].d.ts +16 -0
  12. package/dist/api/admin/users/reactivate/[id].d.ts.map +1 -0
  13. package/dist/api/admin/users/reactivate/[id].js +59 -0
  14. package/dist/api/admin/users/suspend/[id].d.ts +16 -0
  15. package/dist/api/admin/users/suspend/[id].d.ts.map +1 -0
  16. package/dist/api/admin/users/suspend/[id].js +59 -0
  17. package/dist/api/admin/users-by-source.d.ts.map +1 -1
  18. package/dist/api/admin/users-by-source.js +2 -1
  19. package/dist/api/admin/users.d.ts.map +1 -1
  20. package/dist/api/admin/users.js +53 -2
  21. package/dist/api/auth/account/email-change.d.ts +7 -0
  22. package/dist/api/auth/account/email-change.d.ts.map +1 -0
  23. package/dist/api/auth/account/email-change.js +39 -0
  24. package/dist/api/auth/account/reset-password.d.ts +7 -0
  25. package/dist/api/auth/account/reset-password.d.ts.map +1 -0
  26. package/dist/api/auth/account/reset-password.js +36 -0
  27. package/dist/api/auth/check-username.d.ts +9 -0
  28. package/dist/api/auth/check-username.d.ts.map +1 -0
  29. package/dist/api/auth/check-username.js +35 -0
  30. package/dist/api/auth/establish-session.d.ts +2 -0
  31. package/dist/api/auth/establish-session.d.ts.map +1 -0
  32. package/dist/api/auth/establish-session.js +23 -0
  33. package/dist/api/auth/me.d.ts +4 -4
  34. package/dist/api/auth/me.d.ts.map +1 -1
  35. package/dist/api/auth/me.js +28 -6
  36. package/dist/api/auth/profile.d.ts.map +1 -1
  37. package/dist/api/auth/profile.js +6 -3
  38. package/dist/api/auth/storage/recalculate.d.ts +15 -0
  39. package/dist/api/auth/storage/recalculate.d.ts.map +1 -0
  40. package/dist/api/auth/storage/recalculate.js +68 -0
  41. package/dist/api/auth/storage/usage.d.ts +10 -0
  42. package/dist/api/auth/storage/usage.d.ts.map +1 -0
  43. package/dist/api/auth/storage/usage.js +86 -0
  44. package/dist/api/public/add-welcome-bonus.d.ts +16 -0
  45. package/dist/api/public/add-welcome-bonus.d.ts.map +1 -0
  46. package/dist/api/public/add-welcome-bonus.js +177 -0
  47. package/dist/api/public/callback.d.ts +3 -0
  48. package/dist/api/public/callback.d.ts.map +1 -0
  49. package/dist/api/public/callback.js +197 -0
  50. package/dist/api/public/reset-password.d.ts +3 -0
  51. package/dist/api/public/reset-password.d.ts.map +1 -0
  52. package/dist/api/public/reset-password.js +43 -0
  53. package/dist/api/public/set-session.d.ts +7 -0
  54. package/dist/api/public/set-session.d.ts.map +1 -0
  55. package/dist/api/public/set-session.js +55 -0
  56. package/dist/api/public/signin.d.ts.map +1 -1
  57. package/dist/api/public/signin.js +31 -0
  58. package/dist/api/public/signup.d.ts.map +1 -1
  59. package/dist/api/public/signup.js +38 -27
  60. package/dist/api/public/webhook/storage-addon.d.ts +9 -0
  61. package/dist/api/public/webhook/storage-addon.d.ts.map +1 -0
  62. package/dist/api/public/webhook/storage-addon.js +155 -0
  63. package/dist/api/storage.js +2 -2
  64. package/dist/auth.build.config.d.ts.map +1 -1
  65. package/dist/auth.build.config.js +126 -11
  66. package/dist/components/AccountButton.d.ts.map +1 -1
  67. package/dist/components/AccountButton.js +54 -28
  68. package/dist/components/Doc.d.ts.map +1 -1
  69. package/dist/components/Doc.js +1 -1
  70. package/dist/components/HasProfil.d.ts +4 -0
  71. package/dist/components/HasProfil.d.ts.map +1 -0
  72. package/dist/components/HasProfil.js +39 -0
  73. package/dist/components/auth/dashboard.d.ts +1 -1
  74. package/dist/components/auth/dashboard.d.ts.map +1 -1
  75. package/dist/components/auth/dashboard.js +34 -7
  76. package/dist/index.d.ts +2 -0
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +2 -0
  79. package/dist/lib/app-branding-data.d.ts +22 -0
  80. package/dist/lib/app-branding-data.d.ts.map +1 -0
  81. package/dist/lib/app-branding-data.js +49 -0
  82. package/dist/lib/auth-email-service.d.ts +57 -0
  83. package/dist/lib/auth-email-service.d.ts.map +1 -0
  84. package/dist/lib/auth-email-service.js +382 -0
  85. package/dist/lib/auth-email-templates.d.ts +2 -0
  86. package/dist/lib/auth-email-templates.d.ts.map +1 -0
  87. package/dist/lib/auth-email-templates.js +1 -0
  88. package/dist/lib/site-url.d.ts +3 -0
  89. package/dist/lib/site-url.d.ts.map +1 -0
  90. package/dist/lib/site-url.js +11 -0
  91. package/dist/sitemap/manifest.d.ts +9 -0
  92. package/dist/sitemap/manifest.d.ts.map +1 -0
  93. package/dist/sitemap/manifest.js +14 -0
  94. package/dist/web/admin/signup-stats.js +3 -3
  95. package/dist/web/admin/user-detail.d.ts.map +1 -1
  96. package/dist/web/admin/user-detail.js +135 -14
  97. package/dist/web/admin/users-by-signup-source.js +2 -2
  98. package/dist/web/admin/users.d.ts.map +1 -1
  99. package/dist/web/admin/users.js +26 -7
  100. package/dist/web/auth/folder.d.ts.map +1 -1
  101. package/dist/web/auth/folder.js +4 -3
  102. package/dist/web/auth/profile.d.ts.map +1 -1
  103. package/dist/web/auth/profile.js +132 -13
  104. package/dist/web/auth/reglage.d.ts.map +1 -1
  105. package/dist/web/auth/reglage.js +15 -8
  106. package/dist/web/public/ResetPassword.d.ts.map +1 -1
  107. package/dist/web/public/ResetPassword.js +172 -2
  108. package/dist/web/public/SignInPage.d.ts.map +1 -1
  109. package/dist/web/public/SignInPage.js +39 -3
  110. package/dist/web/public/SignUpPage.d.ts.map +1 -1
  111. package/dist/web/public/SignUpPage.js +7 -2
  112. package/dist/web/public/auth-code-error.d.ts +2 -0
  113. package/dist/web/public/auth-code-error.d.ts.map +1 -0
  114. package/dist/web/public/auth-code-error.js +14 -0
  115. package/dist/web/public/confirm.d.ts +2 -0
  116. package/dist/web/public/confirm.d.ts.map +1 -0
  117. package/dist/web/public/confirm.js +157 -0
  118. package/package.json +10 -5
  119. package/src/api/admin/signup-stats.ts +2 -1
  120. package/src/api/admin/storage/usage.ts +141 -0
  121. package/src/api/admin/users/[id]/notifications.ts +3 -2
  122. package/src/api/admin/users/[id].ts +3 -2
  123. package/src/api/admin/users/reactivate/[id].ts +88 -0
  124. package/src/api/admin/users/suspend/[id].ts +85 -0
  125. package/src/api/admin/users-by-source.ts +2 -1
  126. package/src/api/admin/users.ts +59 -2
  127. package/src/api/auth/account/email-change.ts +54 -0
  128. package/src/api/auth/account/reset-password.ts +47 -0
  129. package/src/api/auth/check-username.ts +52 -0
  130. package/src/api/auth/establish-session.ts +32 -0
  131. package/src/api/auth/me.ts +29 -7
  132. package/src/api/auth/profile.ts +6 -2
  133. package/src/api/auth/storage/recalculate.ts +108 -0
  134. package/src/api/auth/storage/usage.ts +113 -0
  135. package/src/api/public/add-welcome-bonus.ts +229 -0
  136. package/src/api/public/callback.ts +307 -0
  137. package/src/api/public/reset-password.ts +52 -0
  138. package/src/api/public/set-session.ts +73 -0
  139. package/src/api/public/signin.ts +36 -0
  140. package/src/api/public/signup.ts +44 -37
  141. package/src/api/public/webhook/storage-addon.ts +267 -0
  142. package/src/api/storage.ts +2 -2
  143. package/src/auth.build.config.ts +126 -11
  144. package/src/components/AccountButton.tsx +114 -90
  145. package/src/components/Doc.tsx +47 -9
  146. package/src/components/HasProfil.tsx +63 -0
  147. package/src/components/auth/dashboard.tsx +54 -13
  148. package/src/i18n/en.json +76 -8
  149. package/src/i18n/es.json +330 -0
  150. package/src/i18n/fr.json +74 -8
  151. package/src/index.ts +2 -0
  152. package/src/lib/app-branding-data.ts +90 -0
  153. package/src/lib/auth-email-service.ts +508 -0
  154. package/src/lib/auth-email-templates.ts +5 -0
  155. package/src/lib/site-url.ts +17 -0
  156. package/src/sitemap/manifest.ts +26 -0
  157. package/src/web/admin/signup-stats.tsx +3 -3
  158. package/src/web/admin/user-detail.tsx +314 -15
  159. package/src/web/admin/users-by-signup-source.tsx +2 -2
  160. package/src/web/admin/users.tsx +50 -14
  161. package/src/web/auth/folder.tsx +23 -5
  162. package/src/web/auth/profile.tsx +227 -13
  163. package/src/web/auth/reglage.tsx +55 -24
  164. package/src/web/public/ResetPassword.tsx +301 -1
  165. package/src/web/public/SignInPage.tsx +43 -3
  166. package/src/web/public/SignUpPage.tsx +14 -5
  167. package/src/web/public/auth-code-error.tsx +49 -0
  168. package/src/web/public/confirm.tsx +195 -0
  169. package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +3 -1
  170. package/supabase/migrations/20251127100000_rename_body_to_message.sql +8 -3
  171. package/supabase/migrations/20260120150001_add_user_storage.sql +105 -0
  172. package/supabase/migrations/20260122131200_add_global_addons_system.sql +305 -0
  173. package/supabase/migrations/20260123100000_enable_vector_extension.sql +9 -0
  174. package/supabase/migrations/20260123140000_module_auth_fix_get_user_limits_null.sql +93 -0
  175. package/supabase/migrations/20260123145000_module_auth_fix_circular_storage_base.sql +89 -0
  176. package/supabase/migrations/20260129400000_add_username_to_user_profil.sql +33 -0
  177. package/dist/web/auth/dashboard.d.ts +0 -2
  178. package/dist/web/auth/dashboard.d.ts.map +0 -1
  179. package/dist/web/auth/dashboard.js +0 -48
@@ -0,0 +1,307 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import {
3
+ getSupabaseServerClient,
4
+ getSupabaseServiceClient,
5
+ } from "@lastbrain/core/server";
6
+ import { logger } from "@lastbrain/core";
7
+ import {
8
+ sendWelcomeOnboardingEmail,
9
+ sendNewUserNotificationToAdmin,
10
+ } from "../../lib/auth-email-service";
11
+
12
+ export async function GET(request: NextRequest) {
13
+ const { searchParams } = new URL(request.url);
14
+
15
+ const code = searchParams.get("code");
16
+ const next = searchParams.get("next") ?? "/";
17
+ const error = searchParams.get("error");
18
+ const errorDescription = searchParams.get("error_description");
19
+
20
+ // If there's an error from Supabase, redirect to error page
21
+ if (error) {
22
+ logger.error("Auth callback error:", error, errorDescription);
23
+ return NextResponse.redirect(
24
+ new URL(
25
+ `/auth-code-error?error=${encodeURIComponent(
26
+ error
27
+ )}&description=${encodeURIComponent(errorDescription || "")}`,
28
+ request.url
29
+ )
30
+ );
31
+ }
32
+
33
+ // If we have a code, exchange it server-side for a session
34
+ if (code) {
35
+ try {
36
+ // Get Supabase server client with proper cookie handlers
37
+ const supabase = await getSupabaseServerClient();
38
+
39
+ // Exchange code for session - cookies are set automatically via @supabase/ssr
40
+ const { data, error: exchangeError } =
41
+ await supabase.auth.exchangeCodeForSession(code);
42
+
43
+ if (exchangeError || !data?.session) {
44
+ logger.error("Code exchange failed:", exchangeError);
45
+ return NextResponse.redirect(
46
+ new URL(
47
+ `/auth-code-error?error=${encodeURIComponent(
48
+ exchangeError?.message || "Failed to exchange code for session"
49
+ )}`,
50
+ request.url
51
+ )
52
+ );
53
+ }
54
+
55
+ logger.debug(
56
+ "✅ Session established successfully for user:",
57
+ data.user?.email
58
+ );
59
+
60
+ // Add welcome bonus when email is confirmed
61
+ if (data.user?.id) {
62
+ try {
63
+ // Check if welcome bonus is enabled via env var
64
+ const ENABLE_SIGNUP_BONUS =
65
+ process.env.NEXT_PUBLIC_ENABLE_SIGNUP_BONUS === "true";
66
+ const SIGNUP_BONUS_AMOUNT_USD = parseFloat(
67
+ process.env.NEXT_PUBLIC_SIGNUP_BONUS_AMOUNT_USD || "1.0"
68
+ );
69
+
70
+ logger.debug(
71
+ `[callback] Signup bonus config - enabled: ${ENABLE_SIGNUP_BONUS}, amount: ${SIGNUP_BONUS_AMOUNT_USD}`
72
+ );
73
+
74
+ if (!ENABLE_SIGNUP_BONUS) {
75
+ logger.debug(
76
+ "Signup bonus disabled via NEXT_PUBLIC_ENABLE_SIGNUP_BONUS"
77
+ );
78
+ } else {
79
+ const serviceClient = await getSupabaseServiceClient();
80
+
81
+ // Check if we already added bonus for this user
82
+ const { data: existingBonus } = await serviceClient
83
+ .from("user_token_ledger")
84
+ .select("id")
85
+ .eq("owner_id", data.user.id)
86
+ .eq("type", "signup_bonus")
87
+ .single();
88
+
89
+ logger.debug(
90
+ `[callback] Existing bonus check for user ${data.user.id}:`,
91
+ existingBonus ? "Already exists" : "Not found"
92
+ );
93
+
94
+ // Only add bonus if not already added
95
+ if (!existingBonus) {
96
+ // Add welcome bonus using wallet system
97
+ const MARGIN_TARGET = 0.6;
98
+ const providerBudgetUsd =
99
+ SIGNUP_BONUS_AMOUNT_USD * (1 - MARGIN_TARGET); // 40%
100
+
101
+ logger.debug(
102
+ `[callback] Inserting bonus for user ${data.user.id}: sell=$${SIGNUP_BONUS_AMOUNT_USD}, provider=$${providerBudgetUsd}`
103
+ );
104
+
105
+ // Insert into ledger with USD values
106
+ const { error: ledgerError } = await serviceClient
107
+ .from("user_token_ledger")
108
+ .insert({
109
+ owner_id: data.user.id,
110
+ type: "signup_bonus",
111
+ amount: 0, // Tokens deprecated
112
+ sell_value_added_usd: SIGNUP_BONUS_AMOUNT_USD,
113
+ provider_budget_added_usd: providerBudgetUsd,
114
+ meta: {
115
+ reason: "signup_welcome_bonus",
116
+ provider: "system",
117
+ },
118
+ created_by: null,
119
+ });
120
+
121
+ if (ledgerError) {
122
+ logger.error(
123
+ "[callback] Error adding welcome bonus to ledger:",
124
+ ledgerError
125
+ );
126
+ } else {
127
+ logger.debug(
128
+ `[callback] ✅ Bonus inserted in ledger for user ${data.user.id}`
129
+ );
130
+ // Update wallet table
131
+ const { data: currentWallet } = await serviceClient
132
+ .from("user_token_wallet")
133
+ .select("wallet_provider_budget_usd, wallet_sell_value_usd")
134
+ .eq("user_id", data.user.id)
135
+ .single();
136
+
137
+ const newProviderBudget =
138
+ (currentWallet?.wallet_provider_budget_usd || 0) +
139
+ providerBudgetUsd;
140
+ const newSellValue =
141
+ (currentWallet?.wallet_sell_value_usd || 0) +
142
+ SIGNUP_BONUS_AMOUNT_USD;
143
+
144
+ const { error: walletError } = await serviceClient
145
+ .from("user_token_wallet")
146
+ .upsert(
147
+ {
148
+ user_id: data.user.id,
149
+ wallet_provider_budget_usd: newProviderBudget,
150
+ wallet_sell_value_usd: newSellValue,
151
+ updated_at: new Date().toISOString(),
152
+ },
153
+ {
154
+ onConflict: "user_id",
155
+ }
156
+ );
157
+
158
+ if (walletError) {
159
+ logger.error(
160
+ "Error updating wallet for welcome bonus:",
161
+ walletError
162
+ );
163
+ } else {
164
+ logger.debug(
165
+ `✅ Added $${SIGNUP_BONUS_AMOUNT_USD} welcome bonus to user ${data.user.id}`
166
+ );
167
+ }
168
+ }
169
+ }
170
+
171
+ // Extract locale from user metadata or default to 'en'
172
+ const locale = (data.user.user_metadata?.locale as string) || "en";
173
+
174
+ logger.debug(
175
+ `[callback] User locale detected: ${locale} (from metadata: ${data.user.user_metadata?.locale || "not set"})`
176
+ );
177
+
178
+ // Add welcome notifications
179
+ const notifications = [
180
+ {
181
+ title: locale === "fr" ? "Bienvenue ! 👋" : "Welcome! 👋",
182
+ body:
183
+ locale === "fr"
184
+ ? "Merci de vous être inscrit ! Explorez toutes les fonctionnalités disponibles."
185
+ : "Thank you for signing up! Explore all available features.",
186
+ type: "primary",
187
+ },
188
+ ];
189
+
190
+ // Add signup bonus notification only if enabled
191
+ if (ENABLE_SIGNUP_BONUS && SIGNUP_BONUS_AMOUNT_USD > 0) {
192
+ notifications.push({
193
+ title:
194
+ locale === "fr"
195
+ ? `🎁 Cadeau de bienvenue : $${SIGNUP_BONUS_AMOUNT_USD}`
196
+ : `🎁 Welcome gift: $${SIGNUP_BONUS_AMOUNT_USD}`,
197
+ body:
198
+ locale === "fr"
199
+ ? `Nous vous offrons $${SIGNUP_BONUS_AMOUNT_USD} de crédit IA pour démarrer. Utilisez-les pour accéder à nos services premium.`
200
+ : `We're giving you $${SIGNUP_BONUS_AMOUNT_USD} AI credits to get started. Use them to access our premium services.`,
201
+ type: "success",
202
+ });
203
+ }
204
+
205
+ for (const notification of notifications) {
206
+ const { error: notifError } = await serviceClient
207
+ .from("user_notifications")
208
+ .insert({
209
+ owner_id: data.user.id,
210
+ title: notification.title,
211
+ body: notification.body,
212
+ type: notification.type,
213
+ });
214
+
215
+ if (notifError) {
216
+ logger.error(
217
+ "[callback] Error creating welcome notification:",
218
+ notifError
219
+ );
220
+ } else {
221
+ logger.debug(
222
+ `[callback] ✅ Notification created: ${notification.title}`
223
+ );
224
+ }
225
+ }
226
+
227
+ logger.debug(
228
+ `✅ Created welcome notifications for user ${data.user.id}`
229
+ );
230
+
231
+ // Send welcome onboarding email with bonus amount if enabled
232
+ try {
233
+ await sendWelcomeOnboardingEmail({
234
+ email: data.user.email || "",
235
+ displayName:
236
+ (data.user.user_metadata?.full_name as string) || "user",
237
+ locale,
238
+ ownerId: data.user.id,
239
+ bonusAmountUsd:
240
+ ENABLE_SIGNUP_BONUS && SIGNUP_BONUS_AMOUNT_USD > 0
241
+ ? SIGNUP_BONUS_AMOUNT_USD
242
+ : undefined,
243
+ });
244
+ logger.debug(
245
+ `📧 Sent welcome onboarding email to ${data.user.email}`
246
+ );
247
+ } catch (emailError) {
248
+ logger.warn(
249
+ "Failed to send welcome email (non-blocking):",
250
+ emailError
251
+ );
252
+ // Don't fail the response if email fails
253
+ }
254
+
255
+ // Send notification to admin about new user signup
256
+ try {
257
+ await sendNewUserNotificationToAdmin({
258
+ userEmail: data.user.email || "",
259
+ displayName:
260
+ (data.user.user_metadata?.full_name as string) || undefined,
261
+ userId: data.user.id,
262
+ locale,
263
+ bonusAmountUsd:
264
+ ENABLE_SIGNUP_BONUS && SIGNUP_BONUS_AMOUNT_USD > 0
265
+ ? SIGNUP_BONUS_AMOUNT_USD
266
+ : undefined,
267
+ });
268
+ logger.debug(
269
+ `📧 Sent admin notification for new user: ${data.user.email}`
270
+ );
271
+ } catch (adminEmailError) {
272
+ logger.warn(
273
+ "Failed to send admin notification email (non-blocking):",
274
+ adminEmailError
275
+ );
276
+ // Don't fail the response if admin email fails
277
+ }
278
+ }
279
+ } catch (bonusError) {
280
+ logger.error(
281
+ "Error adding welcome bonus (non-blocking):",
282
+ bonusError
283
+ );
284
+ // Don't fail auth if bonus creation fails
285
+ }
286
+ }
287
+
288
+ // Success! Cookies have been set via the @supabase/ssr cookie handlers
289
+ // Redirect to destination
290
+ return NextResponse.redirect(new URL(next, request.url));
291
+ } catch (err) {
292
+ logger.error("Unexpected callback error:", err);
293
+ return NextResponse.redirect(
294
+ new URL(
295
+ `/auth-code-error?error=${encodeURIComponent(
296
+ "Unexpected error during authentication"
297
+ )}`,
298
+ request.url
299
+ )
300
+ );
301
+ }
302
+ }
303
+
304
+ // No code or error, redirect to next or home
305
+ logger.warn("Auth callback called without code or error");
306
+ return NextResponse.redirect(new URL(next, request.url));
307
+ }
@@ -0,0 +1,52 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { sendPasswordResetEmail } from "../../lib/auth-email-service";
3
+ import { logger } from "@lastbrain/core";
4
+ import { resolveSiteUrl } from "../../lib/site-url";
5
+
6
+ function extractLocaleFromRequest(request: NextRequest): string | undefined {
7
+ // 1. Priorité : cookie NEXT_LOCALE
8
+ const localeCookie = request.cookies.get("NEXT_LOCALE")?.value;
9
+ if (localeCookie && /^[a-z]{2}$/.test(localeCookie)) {
10
+ return localeCookie;
11
+ }
12
+
13
+ // 2. Fallback : header accept-language
14
+ const lang = request.headers
15
+ .get("accept-language")
16
+ ?.split(",")?.[0]
17
+ ?.split("-")?.[0];
18
+ return lang === "fr" || lang === "en" ? lang : undefined;
19
+ }
20
+
21
+ const DEFAULT_LOCALE = "fr";
22
+
23
+ export async function POST(request: NextRequest) {
24
+ try {
25
+ const locale = extractLocaleFromRequest(request);
26
+ const body = await request.json();
27
+ const { email, next } = body;
28
+
29
+ const resolvedLocale = locale || DEFAULT_LOCALE;
30
+ const baseRedirect = `${resolveSiteUrl(request)}/${resolvedLocale}/reset-password`;
31
+ const redirectTo = next
32
+ ? `${baseRedirect}?next=${encodeURIComponent(next)}`
33
+ : baseRedirect;
34
+
35
+ if (!email) {
36
+ return NextResponse.json({ error: "Email requis" }, { status: 400 });
37
+ }
38
+
39
+ logger.debug("[reset-password] computed redirectTo", { redirectTo, next });
40
+ await sendPasswordResetEmail({ email, locale, redirectTo, next });
41
+
42
+ const respBody: any = { message: "Lien de réinitialisation envoyé" };
43
+ if (process.env.NODE_ENV !== "production") respBody.redirectTo = redirectTo;
44
+ return NextResponse.json(respBody);
45
+ } catch (error) {
46
+ logger.error("reset-password error:", error);
47
+ return NextResponse.json(
48
+ { error: "Impossible d'envoyer le lien de réinitialisation" },
49
+ { status: 500 }
50
+ );
51
+ }
52
+ }
@@ -0,0 +1,73 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSupabaseServerClient } from "@lastbrain/core/server";
3
+ import { logger } from "@lastbrain/core";
4
+
5
+ export async function POST(request: NextRequest) {
6
+ try {
7
+ const payload = await request.json().catch((e) => {
8
+ logger.error("[set-session] failed to parse body", e);
9
+ return {};
10
+ });
11
+
12
+ const { access_token, refresh_token } = payload as {
13
+ access_token?: string;
14
+ refresh_token?: string;
15
+ };
16
+
17
+ logger.debug("[set-session] incoming payload", {
18
+ hasAccessToken: !!access_token,
19
+ hasRefreshToken: !!refresh_token,
20
+ url: request.url,
21
+ headers: {
22
+ host: request.headers.get("host"),
23
+ origin: request.headers.get("origin"),
24
+ referer: request.headers.get("referer"),
25
+ },
26
+ });
27
+
28
+ if (!access_token || !refresh_token) {
29
+ logger.warn("[set-session] missing tokens", { payload });
30
+ return NextResponse.json({ error: "Missing tokens" }, { status: 400 });
31
+ }
32
+
33
+ const supabase = await getSupabaseServerClient();
34
+
35
+ logger.debug("[set-session] calling supabase.auth.setSession");
36
+ const { data, error } = await supabase.auth
37
+ .setSession({
38
+ access_token,
39
+ refresh_token,
40
+ })
41
+ .catch((e) => {
42
+ logger.error("[set-session] supabase.setSession threw", e);
43
+ return { data: null, error: { message: String(e) } };
44
+ });
45
+
46
+ logger.debug("[set-session] supabase response", {
47
+ hasSession: !!data?.session,
48
+ userId: data?.session?.user?.id,
49
+ error: error?.message,
50
+ });
51
+
52
+ if (error || !data?.session) {
53
+ logger.warn("[set-session] failed to set session", { error });
54
+ return NextResponse.json(
55
+ { error: error?.message || "Failed to set session" },
56
+ { status: 401 }
57
+ );
58
+ }
59
+
60
+ // Cookies are set by the Supabase SSR adapter inside setSession
61
+ logger.debug("[set-session] session set successfully for user", {
62
+ userId: data.session.user.id,
63
+ });
64
+
65
+ return NextResponse.json({ success: true });
66
+ } catch (err: any) {
67
+ logger.error("[set-session] unexpected error", err);
68
+ return NextResponse.json(
69
+ { error: err?.message || "Unexpected error" },
70
+ { status: 500 }
71
+ );
72
+ }
73
+ }
@@ -27,5 +27,41 @@ export async function POST(request: Request) {
27
27
  return jsonResponse({ error: error.message }, 400);
28
28
  }
29
29
 
30
+ // Si le client a envoyé un cookie NEXT_LOCALE, synchroniser côté serveur
31
+ // la préférence stockée en base (user_profil.language). Ne rien faire
32
+ // si le cookie n'existe pas.
33
+ try {
34
+ const cookieHeader = request.headers.get("cookie") || "";
35
+ const match = cookieHeader.match(/(?:^|; )NEXT_LOCALE=([^;]+)/);
36
+ const currentCookie = match ? decodeURIComponent(match[1]) : null;
37
+
38
+ if (currentCookie && data?.user?.id) {
39
+ const { data: profil, error: _profilErr } = await supabase
40
+ .from("user_profil")
41
+ .select("language")
42
+ .eq("user_id", data.user.id)
43
+ .maybeSingle();
44
+
45
+ const profileLang = profil?.language;
46
+ if (profileLang && profileLang !== currentCookie) {
47
+ const maxAge = 365 * 24 * 60 * 60; // 1 an
48
+ const cookieValue = `NEXT_LOCALE=${encodeURIComponent(
49
+ profileLang
50
+ )}; Path=/; Max-Age=${maxAge}; SameSite=Lax`;
51
+
52
+ return new Response(JSON.stringify({ data, updated: true }), {
53
+ headers: {
54
+ "content-type": "application/json",
55
+ "set-cookie": cookieValue,
56
+ },
57
+ status: 200,
58
+ });
59
+ }
60
+ }
61
+ } catch (e) {
62
+ // ne pas bloquer la connexion si la sync échoue
63
+ console.debug("signin: sync-locale failed", e);
64
+ }
65
+
30
66
  return jsonResponse({ data });
31
67
  }
@@ -1,21 +1,30 @@
1
- import {
2
- getSupabaseServerClient,
3
- getSupabaseServiceClient,
4
- } from "@lastbrain/core/server";
1
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
5
2
  import { NextRequest, NextResponse } from "next/server";
3
+ import { sendSignupConfirmationEmail } from "../../lib/auth-email-service";
4
+ import { logger } from "@lastbrain/core";
5
+ import { resolveSiteUrl } from "../../lib/site-url";
6
+
7
+ function extractLocale(request: NextRequest): string | undefined {
8
+ const lang = request.headers
9
+ .get("accept-language")
10
+ ?.split(",")?.[0]
11
+ ?.split("-")?.[0];
12
+ return lang === "fr" || lang === "en" ? lang : undefined;
13
+ }
6
14
 
7
15
  interface SignUpRequest {
8
16
  email: string;
9
17
  password: string;
10
18
  fullName: string;
11
19
  signupSource?: string; // 'lastbrain' or 'recipe'
20
+ next?: string;
12
21
  }
13
22
 
14
23
  export async function POST(request: NextRequest) {
15
24
  try {
16
25
  const body: SignUpRequest = await request.json();
26
+ const locale = extractLocale(request);
17
27
  const defaultSource = process.env.APP_NAME || "undefined";
18
- console.log("🚀 ~ POST ~ defaultSource:", defaultSource);
19
28
  const { email, password, fullName, signupSource = defaultSource } = body;
20
29
 
21
30
  // Validate required fields
@@ -26,41 +35,38 @@ export async function POST(request: NextRequest) {
26
35
  );
27
36
  }
28
37
 
29
- // Get Supabase client for authentication
30
- const supabase = await getSupabaseServerClient();
31
-
32
- // Sign up the user
33
- const { data: authData, error: signUpError } = await supabase.auth.signUp({
34
- email,
35
- password,
36
- options: {
37
- emailRedirectTo: `${request.nextUrl.origin}/api/auth/callback`,
38
- data: {
39
- full_name: fullName,
40
- signup_source: signupSource,
41
- },
42
- },
43
- });
44
-
45
- if (signUpError) {
46
- return NextResponse.json({ error: signUpError.message }, { status: 400 });
47
- }
48
-
49
- if (!authData.user) {
50
- return NextResponse.json(
51
- { error: "Erreur lors de la création du compte" },
52
- { status: 500 }
53
- );
38
+ // Générer le lien d'inscription + envoyer l'email via module-contact-pro
39
+ let user;
40
+ try {
41
+ user = await sendSignupConfirmationEmail({
42
+ email,
43
+ password,
44
+ locale,
45
+ fullName,
46
+ signupSource,
47
+ // Use client-side confirm page so Supabase can return hash tokens
48
+ // and the client `ConfirmPage` will set the session and add welcome bonus.
49
+ redirectTo: `${resolveSiteUrl(request)}/confirm`,
50
+ next: body.next,
51
+ });
52
+ } catch (emailError: any) {
53
+ // Handle Supabase auth errors
54
+ if (emailError?.status === 422 && emailError?.code === "email_exists") {
55
+ return NextResponse.json(
56
+ { error: "Cet email est déjà utilisé." },
57
+ { status: 409 }
58
+ );
59
+ }
60
+ throw emailError;
54
61
  }
55
62
 
56
- // Create user profile with signup_source
57
63
  const serviceClient = await getSupabaseServiceClient();
58
64
 
59
65
  // Check if profile already exists
60
66
  const { data: existingProfile } = await serviceClient
61
67
  .from("user_profil")
62
68
  .select("owner_id")
63
- .eq("owner_id", authData.user.id)
69
+ .eq("owner_id", user?.id)
64
70
  .single();
65
71
 
66
72
  // Only create profile if it doesn't exist
@@ -68,7 +74,7 @@ export async function POST(request: NextRequest) {
68
74
  const { error: profileError } = await serviceClient
69
75
  .from("user_profil")
70
76
  .insert({
71
- owner_id: authData.user.id,
77
+ owner_id: user?.id,
72
78
  first_name: fullName?.split(" ")[0] || "",
73
79
  last_name: fullName?.split(" ").slice(1).join(" ") || "",
74
80
  signup_source: signupSource,
@@ -76,7 +82,7 @@ export async function POST(request: NextRequest) {
76
82
  });
77
83
 
78
84
  if (profileError) {
79
- console.error("Error creating user profile:", profileError);
85
+ logger.error("Error creating user profile:", profileError);
80
86
  return NextResponse.json(
81
87
  {
82
88
  error: "Compte créé mais profil non configuré",
@@ -90,14 +96,15 @@ export async function POST(request: NextRequest) {
90
96
  return NextResponse.json(
91
97
  {
92
98
  data: {
93
- user: authData.user,
94
- message: "Compte créé avec succès",
99
+ user,
100
+ message:
101
+ "Compte créé avec succès. Vérifiez vos emails pour confirmer votre compte.",
95
102
  },
96
103
  },
97
104
  { status: 201 }
98
105
  );
99
106
  } catch (error) {
100
- console.error("Signup error:", error);
107
+ logger.error("Signup error:", error);
101
108
  return NextResponse.json(
102
109
  { error: "Erreur interne du serveur" },
103
110
  { status: 500 }