@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,197 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getSupabaseServerClient, getSupabaseServiceClient, } from "@lastbrain/core/server";
3
+ import { logger } from "@lastbrain/core";
4
+ import { sendWelcomeOnboardingEmail, sendNewUserNotificationToAdmin, } from "../../lib/auth-email-service";
5
+ export async function GET(request) {
6
+ const { searchParams } = new URL(request.url);
7
+ const code = searchParams.get("code");
8
+ const next = searchParams.get("next") ?? "/";
9
+ const error = searchParams.get("error");
10
+ const errorDescription = searchParams.get("error_description");
11
+ // If there's an error from Supabase, redirect to error page
12
+ if (error) {
13
+ logger.error("Auth callback error:", error, errorDescription);
14
+ return NextResponse.redirect(new URL(`/auth-code-error?error=${encodeURIComponent(error)}&description=${encodeURIComponent(errorDescription || "")}`, request.url));
15
+ }
16
+ // If we have a code, exchange it server-side for a session
17
+ if (code) {
18
+ try {
19
+ // Get Supabase server client with proper cookie handlers
20
+ const supabase = await getSupabaseServerClient();
21
+ // Exchange code for session - cookies are set automatically via @supabase/ssr
22
+ const { data, error: exchangeError } = await supabase.auth.exchangeCodeForSession(code);
23
+ if (exchangeError || !data?.session) {
24
+ logger.error("Code exchange failed:", exchangeError);
25
+ return NextResponse.redirect(new URL(`/auth-code-error?error=${encodeURIComponent(exchangeError?.message || "Failed to exchange code for session")}`, request.url));
26
+ }
27
+ logger.debug("✅ Session established successfully for user:", data.user?.email);
28
+ // Add welcome bonus when email is confirmed
29
+ if (data.user?.id) {
30
+ try {
31
+ // Check if welcome bonus is enabled via env var
32
+ const ENABLE_SIGNUP_BONUS = process.env.NEXT_PUBLIC_ENABLE_SIGNUP_BONUS === "true";
33
+ const SIGNUP_BONUS_AMOUNT_USD = parseFloat(process.env.NEXT_PUBLIC_SIGNUP_BONUS_AMOUNT_USD || "1.0");
34
+ logger.debug(`[callback] Signup bonus config - enabled: ${ENABLE_SIGNUP_BONUS}, amount: ${SIGNUP_BONUS_AMOUNT_USD}`);
35
+ if (!ENABLE_SIGNUP_BONUS) {
36
+ logger.debug("Signup bonus disabled via NEXT_PUBLIC_ENABLE_SIGNUP_BONUS");
37
+ }
38
+ else {
39
+ const serviceClient = await getSupabaseServiceClient();
40
+ // Check if we already added bonus for this user
41
+ const { data: existingBonus } = await serviceClient
42
+ .from("user_token_ledger")
43
+ .select("id")
44
+ .eq("owner_id", data.user.id)
45
+ .eq("type", "signup_bonus")
46
+ .single();
47
+ logger.debug(`[callback] Existing bonus check for user ${data.user.id}:`, existingBonus ? "Already exists" : "Not found");
48
+ // Only add bonus if not already added
49
+ if (!existingBonus) {
50
+ // Add welcome bonus using wallet system
51
+ const MARGIN_TARGET = 0.6;
52
+ const providerBudgetUsd = SIGNUP_BONUS_AMOUNT_USD * (1 - MARGIN_TARGET); // 40%
53
+ logger.debug(`[callback] Inserting bonus for user ${data.user.id}: sell=$${SIGNUP_BONUS_AMOUNT_USD}, provider=$${providerBudgetUsd}`);
54
+ // Insert into ledger with USD values
55
+ const { error: ledgerError } = await serviceClient
56
+ .from("user_token_ledger")
57
+ .insert({
58
+ owner_id: data.user.id,
59
+ type: "signup_bonus",
60
+ amount: 0, // Tokens deprecated
61
+ sell_value_added_usd: SIGNUP_BONUS_AMOUNT_USD,
62
+ provider_budget_added_usd: providerBudgetUsd,
63
+ meta: {
64
+ reason: "signup_welcome_bonus",
65
+ provider: "system",
66
+ },
67
+ created_by: null,
68
+ });
69
+ if (ledgerError) {
70
+ logger.error("[callback] Error adding welcome bonus to ledger:", ledgerError);
71
+ }
72
+ else {
73
+ logger.debug(`[callback] ✅ Bonus inserted in ledger for user ${data.user.id}`);
74
+ // Update wallet table
75
+ const { data: currentWallet } = await serviceClient
76
+ .from("user_token_wallet")
77
+ .select("wallet_provider_budget_usd, wallet_sell_value_usd")
78
+ .eq("user_id", data.user.id)
79
+ .single();
80
+ const newProviderBudget = (currentWallet?.wallet_provider_budget_usd || 0) +
81
+ providerBudgetUsd;
82
+ const newSellValue = (currentWallet?.wallet_sell_value_usd || 0) +
83
+ SIGNUP_BONUS_AMOUNT_USD;
84
+ const { error: walletError } = await serviceClient
85
+ .from("user_token_wallet")
86
+ .upsert({
87
+ user_id: data.user.id,
88
+ wallet_provider_budget_usd: newProviderBudget,
89
+ wallet_sell_value_usd: newSellValue,
90
+ updated_at: new Date().toISOString(),
91
+ }, {
92
+ onConflict: "user_id",
93
+ });
94
+ if (walletError) {
95
+ logger.error("Error updating wallet for welcome bonus:", walletError);
96
+ }
97
+ else {
98
+ logger.debug(`✅ Added $${SIGNUP_BONUS_AMOUNT_USD} welcome bonus to user ${data.user.id}`);
99
+ }
100
+ }
101
+ }
102
+ // Extract locale from user metadata or default to 'en'
103
+ const locale = data.user.user_metadata?.locale || "en";
104
+ logger.debug(`[callback] User locale detected: ${locale} (from metadata: ${data.user.user_metadata?.locale || "not set"})`);
105
+ // Add welcome notifications
106
+ const notifications = [
107
+ {
108
+ title: locale === "fr" ? "Bienvenue ! 👋" : "Welcome! 👋",
109
+ body: locale === "fr"
110
+ ? "Merci de vous être inscrit ! Explorez toutes les fonctionnalités disponibles."
111
+ : "Thank you for signing up! Explore all available features.",
112
+ type: "primary",
113
+ },
114
+ ];
115
+ // Add signup bonus notification only if enabled
116
+ if (ENABLE_SIGNUP_BONUS && SIGNUP_BONUS_AMOUNT_USD > 0) {
117
+ notifications.push({
118
+ title: locale === "fr"
119
+ ? `🎁 Cadeau de bienvenue : $${SIGNUP_BONUS_AMOUNT_USD}`
120
+ : `🎁 Welcome gift: $${SIGNUP_BONUS_AMOUNT_USD}`,
121
+ body: locale === "fr"
122
+ ? `Nous vous offrons $${SIGNUP_BONUS_AMOUNT_USD} de crédit IA pour démarrer. Utilisez-les pour accéder à nos services premium.`
123
+ : `We're giving you $${SIGNUP_BONUS_AMOUNT_USD} AI credits to get started. Use them to access our premium services.`,
124
+ type: "success",
125
+ });
126
+ }
127
+ for (const notification of notifications) {
128
+ const { error: notifError } = await serviceClient
129
+ .from("user_notifications")
130
+ .insert({
131
+ owner_id: data.user.id,
132
+ title: notification.title,
133
+ body: notification.body,
134
+ type: notification.type,
135
+ });
136
+ if (notifError) {
137
+ logger.error("[callback] Error creating welcome notification:", notifError);
138
+ }
139
+ else {
140
+ logger.debug(`[callback] ✅ Notification created: ${notification.title}`);
141
+ }
142
+ }
143
+ logger.debug(`✅ Created welcome notifications for user ${data.user.id}`);
144
+ // Send welcome onboarding email with bonus amount if enabled
145
+ try {
146
+ await sendWelcomeOnboardingEmail({
147
+ email: data.user.email || "",
148
+ displayName: data.user.user_metadata?.full_name || "user",
149
+ locale,
150
+ ownerId: data.user.id,
151
+ bonusAmountUsd: ENABLE_SIGNUP_BONUS && SIGNUP_BONUS_AMOUNT_USD > 0
152
+ ? SIGNUP_BONUS_AMOUNT_USD
153
+ : undefined,
154
+ });
155
+ logger.debug(`📧 Sent welcome onboarding email to ${data.user.email}`);
156
+ }
157
+ catch (emailError) {
158
+ logger.warn("Failed to send welcome email (non-blocking):", emailError);
159
+ // Don't fail the response if email fails
160
+ }
161
+ // Send notification to admin about new user signup
162
+ try {
163
+ await sendNewUserNotificationToAdmin({
164
+ userEmail: data.user.email || "",
165
+ displayName: data.user.user_metadata?.full_name || undefined,
166
+ userId: data.user.id,
167
+ locale,
168
+ bonusAmountUsd: ENABLE_SIGNUP_BONUS && SIGNUP_BONUS_AMOUNT_USD > 0
169
+ ? SIGNUP_BONUS_AMOUNT_USD
170
+ : undefined,
171
+ });
172
+ logger.debug(`📧 Sent admin notification for new user: ${data.user.email}`);
173
+ }
174
+ catch (adminEmailError) {
175
+ logger.warn("Failed to send admin notification email (non-blocking):", adminEmailError);
176
+ // Don't fail the response if admin email fails
177
+ }
178
+ }
179
+ }
180
+ catch (bonusError) {
181
+ logger.error("Error adding welcome bonus (non-blocking):", bonusError);
182
+ // Don't fail auth if bonus creation fails
183
+ }
184
+ }
185
+ // Success! Cookies have been set via the @supabase/ssr cookie handlers
186
+ // Redirect to destination
187
+ return NextResponse.redirect(new URL(next, request.url));
188
+ }
189
+ catch (err) {
190
+ logger.error("Unexpected callback error:", err);
191
+ return NextResponse.redirect(new URL(`/auth-code-error?error=${encodeURIComponent("Unexpected error during authentication")}`, request.url));
192
+ }
193
+ }
194
+ // No code or error, redirect to next or home
195
+ logger.warn("Auth callback called without code or error");
196
+ return NextResponse.redirect(new URL(next, request.url));
197
+ }
@@ -0,0 +1,3 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ export declare function POST(request: NextRequest): Promise<NextResponse<any>>;
3
+ //# sourceMappingURL=reset-password.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset-password.d.ts","sourceRoot":"","sources":["../../../src/api/public/reset-password.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAsBxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,8BA6B9C"}
@@ -0,0 +1,43 @@
1
+ import { 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
+ function extractLocaleFromRequest(request) {
6
+ // 1. Priorité : cookie NEXT_LOCALE
7
+ const localeCookie = request.cookies.get("NEXT_LOCALE")?.value;
8
+ if (localeCookie && /^[a-z]{2}$/.test(localeCookie)) {
9
+ return localeCookie;
10
+ }
11
+ // 2. Fallback : header accept-language
12
+ const lang = request.headers
13
+ .get("accept-language")
14
+ ?.split(",")?.[0]
15
+ ?.split("-")?.[0];
16
+ return lang === "fr" || lang === "en" ? lang : undefined;
17
+ }
18
+ const DEFAULT_LOCALE = "fr";
19
+ export async function POST(request) {
20
+ try {
21
+ const locale = extractLocaleFromRequest(request);
22
+ const body = await request.json();
23
+ const { email, next } = body;
24
+ const resolvedLocale = locale || DEFAULT_LOCALE;
25
+ const baseRedirect = `${resolveSiteUrl(request)}/${resolvedLocale}/reset-password`;
26
+ const redirectTo = next
27
+ ? `${baseRedirect}?next=${encodeURIComponent(next)}`
28
+ : baseRedirect;
29
+ if (!email) {
30
+ return NextResponse.json({ error: "Email requis" }, { status: 400 });
31
+ }
32
+ logger.debug("[reset-password] computed redirectTo", { redirectTo, next });
33
+ await sendPasswordResetEmail({ email, locale, redirectTo, next });
34
+ const respBody = { message: "Lien de réinitialisation envoyé" };
35
+ if (process.env.NODE_ENV !== "production")
36
+ respBody.redirectTo = redirectTo;
37
+ return NextResponse.json(respBody);
38
+ }
39
+ catch (error) {
40
+ logger.error("reset-password error:", error);
41
+ return NextResponse.json({ error: "Impossible d'envoyer le lien de réinitialisation" }, { status: 500 });
42
+ }
43
+ }
@@ -0,0 +1,7 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ export declare function POST(request: NextRequest): Promise<NextResponse<{
3
+ success: boolean;
4
+ }> | NextResponse<{
5
+ error: any;
6
+ }>>;
7
+ //# sourceMappingURL=set-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"set-session.d.ts","sourceRoot":"","sources":["../../../src/api/public/set-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;IAoE9C"}
@@ -0,0 +1,55 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getSupabaseServerClient } from "@lastbrain/core/server";
3
+ import { logger } from "@lastbrain/core";
4
+ export async function POST(request) {
5
+ try {
6
+ const payload = await request.json().catch((e) => {
7
+ logger.error("[set-session] failed to parse body", e);
8
+ return {};
9
+ });
10
+ const { access_token, refresh_token } = payload;
11
+ logger.debug("[set-session] incoming payload", {
12
+ hasAccessToken: !!access_token,
13
+ hasRefreshToken: !!refresh_token,
14
+ url: request.url,
15
+ headers: {
16
+ host: request.headers.get("host"),
17
+ origin: request.headers.get("origin"),
18
+ referer: request.headers.get("referer"),
19
+ },
20
+ });
21
+ if (!access_token || !refresh_token) {
22
+ logger.warn("[set-session] missing tokens", { payload });
23
+ return NextResponse.json({ error: "Missing tokens" }, { status: 400 });
24
+ }
25
+ const supabase = await getSupabaseServerClient();
26
+ logger.debug("[set-session] calling supabase.auth.setSession");
27
+ const { data, error } = await supabase.auth
28
+ .setSession({
29
+ access_token,
30
+ refresh_token,
31
+ })
32
+ .catch((e) => {
33
+ logger.error("[set-session] supabase.setSession threw", e);
34
+ return { data: null, error: { message: String(e) } };
35
+ });
36
+ logger.debug("[set-session] supabase response", {
37
+ hasSession: !!data?.session,
38
+ userId: data?.session?.user?.id,
39
+ error: error?.message,
40
+ });
41
+ if (error || !data?.session) {
42
+ logger.warn("[set-session] failed to set session", { error });
43
+ return NextResponse.json({ error: error?.message || "Failed to set session" }, { status: 401 });
44
+ }
45
+ // Cookies are set by the Supabase SSR adapter inside setSession
46
+ logger.debug("[set-session] session set successfully for user", {
47
+ userId: data.session.user.id,
48
+ });
49
+ return NextResponse.json({ success: true });
50
+ }
51
+ catch (err) {
52
+ logger.error("[set-session] unexpected error", err);
53
+ return NextResponse.json({ error: err?.message || "Unexpected error" }, { status: 500 });
54
+ }
55
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"signin.d.ts","sourceRoot":"","sources":["../../../src/api/public/signin.ts"],"names":[],"mappings":"AAWA,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO,qBAmB1C"}
1
+ {"version":3,"file":"signin.d.ts","sourceRoot":"","sources":["../../../src/api/public/signin.ts"],"names":[],"mappings":"AAWA,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO,qBAuD1C"}
@@ -21,5 +21,36 @@ export async function POST(request) {
21
21
  if (error) {
22
22
  return jsonResponse({ error: error.message }, 400);
23
23
  }
24
+ // Si le client a envoyé un cookie NEXT_LOCALE, synchroniser côté serveur
25
+ // la préférence stockée en base (user_profil.language). Ne rien faire
26
+ // si le cookie n'existe pas.
27
+ try {
28
+ const cookieHeader = request.headers.get("cookie") || "";
29
+ const match = cookieHeader.match(/(?:^|; )NEXT_LOCALE=([^;]+)/);
30
+ const currentCookie = match ? decodeURIComponent(match[1]) : null;
31
+ if (currentCookie && data?.user?.id) {
32
+ const { data: profil, error: _profilErr } = await supabase
33
+ .from("user_profil")
34
+ .select("language")
35
+ .eq("user_id", data.user.id)
36
+ .maybeSingle();
37
+ const profileLang = profil?.language;
38
+ if (profileLang && profileLang !== currentCookie) {
39
+ const maxAge = 365 * 24 * 60 * 60; // 1 an
40
+ const cookieValue = `NEXT_LOCALE=${encodeURIComponent(profileLang)}; Path=/; Max-Age=${maxAge}; SameSite=Lax`;
41
+ return new Response(JSON.stringify({ data, updated: true }), {
42
+ headers: {
43
+ "content-type": "application/json",
44
+ "set-cookie": cookieValue,
45
+ },
46
+ status: 200,
47
+ });
48
+ }
49
+ }
50
+ }
51
+ catch (e) {
52
+ // ne pas bloquer la connexion si la sync échoue
53
+ console.debug("signin: sync-locale failed", e);
54
+ }
24
55
  return jsonResponse({ data });
25
56
  }
@@ -1 +1 @@
1
- {"version":3,"file":"signup.d.ts","sourceRoot":"","sources":["../../../src/api/public/signup.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AASxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;;;IA4F9C"}
1
+ {"version":3,"file":"signup.d.ts","sourceRoot":"","sources":["../../../src/api/public/signup.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAqBxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;;;IA0F9C"}
@@ -1,56 +1,67 @@
1
- import { getSupabaseServerClient, getSupabaseServiceClient, } from "@lastbrain/core/server";
1
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
2
2
  import { 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
+ function extractLocale(request) {
7
+ const lang = request.headers
8
+ .get("accept-language")
9
+ ?.split(",")?.[0]
10
+ ?.split("-")?.[0];
11
+ return lang === "fr" || lang === "en" ? lang : undefined;
12
+ }
3
13
  export async function POST(request) {
4
14
  try {
5
15
  const body = await request.json();
16
+ const locale = extractLocale(request);
6
17
  const defaultSource = process.env.APP_NAME || "undefined";
7
- console.log("🚀 ~ POST ~ defaultSource:", defaultSource);
8
18
  const { email, password, fullName, signupSource = defaultSource } = body;
9
19
  // Validate required fields
10
20
  if (!email || !password) {
11
21
  return NextResponse.json({ error: "Email et mot de passe requis." }, { status: 400 });
12
22
  }
13
- // Get Supabase client for authentication
14
- const supabase = await getSupabaseServerClient();
15
- // Sign up the user
16
- const { data: authData, error: signUpError } = await supabase.auth.signUp({
17
- email,
18
- password,
19
- options: {
20
- emailRedirectTo: `${request.nextUrl.origin}/api/auth/callback`,
21
- data: {
22
- full_name: fullName,
23
- signup_source: signupSource,
24
- },
25
- },
26
- });
27
- if (signUpError) {
28
- return NextResponse.json({ error: signUpError.message }, { status: 400 });
23
+ // Générer le lien d'inscription + envoyer l'email via module-contact-pro
24
+ let user;
25
+ try {
26
+ user = await sendSignupConfirmationEmail({
27
+ email,
28
+ password,
29
+ locale,
30
+ fullName,
31
+ signupSource,
32
+ // Use client-side confirm page so Supabase can return hash tokens
33
+ // and the client `ConfirmPage` will set the session and add welcome bonus.
34
+ redirectTo: `${resolveSiteUrl(request)}/confirm`,
35
+ next: body.next,
36
+ });
29
37
  }
30
- if (!authData.user) {
31
- return NextResponse.json({ error: "Erreur lors de la création du compte" }, { status: 500 });
38
+ catch (emailError) {
39
+ // Handle Supabase auth errors
40
+ if (emailError?.status === 422 && emailError?.code === "email_exists") {
41
+ return NextResponse.json({ error: "Cet email est déjà utilisé." }, { status: 409 });
42
+ }
43
+ throw emailError;
32
44
  }
33
- // Create user profile with signup_source
34
45
  const serviceClient = await getSupabaseServiceClient();
35
46
  // Check if profile already exists
36
47
  const { data: existingProfile } = await serviceClient
37
48
  .from("user_profil")
38
49
  .select("owner_id")
39
- .eq("owner_id", authData.user.id)
50
+ .eq("owner_id", user?.id)
40
51
  .single();
41
52
  // Only create profile if it doesn't exist
42
53
  if (!existingProfile) {
43
54
  const { error: profileError } = await serviceClient
44
55
  .from("user_profil")
45
56
  .insert({
46
- owner_id: authData.user.id,
57
+ owner_id: user?.id,
47
58
  first_name: fullName?.split(" ")[0] || "",
48
59
  last_name: fullName?.split(" ").slice(1).join(" ") || "",
49
60
  signup_source: signupSource,
50
61
  preferences: {},
51
62
  });
52
63
  if (profileError) {
53
- console.error("Error creating user profile:", profileError);
64
+ logger.error("Error creating user profile:", profileError);
54
65
  return NextResponse.json({
55
66
  error: "Compte créé mais profil non configuré",
56
67
  message: profileError.message,
@@ -59,13 +70,13 @@ export async function POST(request) {
59
70
  }
60
71
  return NextResponse.json({
61
72
  data: {
62
- user: authData.user,
63
- message: "Compte créé avec succès",
73
+ user,
74
+ message: "Compte créé avec succès. Vérifiez vos emails pour confirmer votre compte.",
64
75
  },
65
76
  }, { status: 201 });
66
77
  }
67
78
  catch (error) {
68
- console.error("Signup error:", error);
79
+ logger.error("Signup error:", error);
69
80
  return NextResponse.json({ error: "Erreur interne du serveur" }, { status: 500 });
70
81
  }
71
82
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * POST /api/auth/webhook/storage-addon
3
+ * Webhook handler for storage addon subscription events
4
+ * Called by module-billing-pro when a storage addon subscription item is updated
5
+ */
6
+ export declare const runtime = "nodejs";
7
+ export declare const dynamic = "force-dynamic";
8
+ export declare function POST(request: Request): Promise<Response>;
9
+ //# sourceMappingURL=storage-addon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-addon.d.ts","sourceRoot":"","sources":["../../../../src/api/public/webhook/storage-addon.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,eAAO,MAAM,OAAO,WAAW,CAAC;AAChC,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAevC,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO,qBAgP1C"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * POST /api/auth/webhook/storage-addon
3
+ * Webhook handler for storage addon subscription events
4
+ * Called by module-billing-pro when a storage addon subscription item is updated
5
+ */
6
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
7
+ import { logger } from "@lastbrain/core";
8
+ // Runtime config
9
+ export const runtime = "nodejs";
10
+ export const dynamic = "force-dynamic";
11
+ export async function POST(request) {
12
+ try {
13
+ const body = await request.json();
14
+ const { eventType, owner_id, addon_key, stripe_subscription_id, stripe_subscription_item_id, stripe_customer_id, quantity = 1, status, current_period_start, current_period_end, } = body;
15
+ logger.debug(`[Storage Addon Webhook] Processing ${eventType} for user ${owner_id}, addon ${addon_key}`);
16
+ if (!owner_id ||
17
+ !addon_key ||
18
+ !stripe_subscription_id ||
19
+ !stripe_subscription_item_id) {
20
+ logger.warn("[Storage Addon Webhook] Missing required fields in payload");
21
+ return Response.json({ received: true }, { status: 200 });
22
+ }
23
+ const supabase = await getSupabaseServiceClient();
24
+ // Find the addon by key
25
+ const { data: addon, error: addonError } = await supabase
26
+ .from("global_addons")
27
+ .select("id")
28
+ .eq("addon_key", addon_key)
29
+ .eq("addon_type", "storage")
30
+ .eq("is_active", true)
31
+ .single();
32
+ if (addonError || !addon) {
33
+ logger.warn(`[Storage Addon Webhook] Addon not found for key: ${addon_key}`);
34
+ return Response.json({ received: true }, { status: 200 });
35
+ }
36
+ // Handle different event types
37
+ switch (eventType) {
38
+ case "addon.created":
39
+ case "addon.updated": {
40
+ // Upsert user addon activation
41
+ // Use owner_id + addon_id as the unique key (one addon type per user)
42
+ // When a subscription is canceled and recreated, we update the same record with new stripe IDs
43
+ const { error: upsertError } = await supabase
44
+ .from("user_global_addons")
45
+ .upsert({
46
+ owner_id,
47
+ addon_id: addon.id,
48
+ quantity,
49
+ status,
50
+ current_period_start,
51
+ current_period_end,
52
+ stripe_customer_id,
53
+ stripe_subscription_id,
54
+ stripe_subscription_item_id,
55
+ updated_at: new Date().toISOString(),
56
+ }, {
57
+ // Use owner_id + addon_id as the unique key for idempotency
58
+ // This ensures one addon per user, even if subscription is recreated
59
+ onConflict: "owner_id,addon_id",
60
+ });
61
+ if (upsertError) {
62
+ logger.error("[Storage Addon Webhook] Error upserting addon:", upsertError);
63
+ return Response.json({ error: "Failed to update storage addon" }, { status: 500 });
64
+ }
65
+ logger.debug(`[Storage Addon Webhook] Storage addon ${addon_key} ${eventType} for user ${owner_id}: status=${status}, quantity=${quantity}`);
66
+ // Update user_profil.stockage to reflect total (optional, since get_user_limits calculates dynamically)
67
+ // We'll keep this for backward compatibility with existing code
68
+ try {
69
+ const limitsResult = await supabase.rpc("get_user_limits", {
70
+ owner_id_param: owner_id,
71
+ });
72
+ logger.debug(`[Storage Addon Webhook] get_user_limits raw result:`, JSON.stringify(limitsResult, null, 2));
73
+ if (limitsResult.error) {
74
+ logger.error(`[Storage Addon Webhook] get_user_limits error:`, limitsResult.error);
75
+ }
76
+ if (limitsResult.data) {
77
+ // Supabase RPC returns JSON as string, need to parse if string
78
+ const limitsData = typeof limitsResult.data === "string"
79
+ ? JSON.parse(limitsResult.data)
80
+ : limitsResult.data;
81
+ logger.debug(`[Storage Addon Webhook] Parsed limits data:`, limitsData);
82
+ const totalStorageGb = limitsData.storage_quota_gb;
83
+ logger.debug(`[Storage Addon Webhook] Extracted storage_quota_gb: ${totalStorageGb}`);
84
+ // Update user_profil with new total
85
+ await supabase.from("user_profil").upsert({
86
+ owner_id,
87
+ stockage: totalStorageGb,
88
+ updated_at: new Date().toISOString(),
89
+ }, {
90
+ onConflict: "owner_id",
91
+ });
92
+ logger.debug(`[Storage Addon Webhook] Updated user_profil.stockage to ${totalStorageGb} GB for user ${owner_id}`);
93
+ }
94
+ else {
95
+ logger.warn(`[Storage Addon Webhook] get_user_limits returned no data for user ${owner_id}`);
96
+ }
97
+ }
98
+ catch (error) {
99
+ logger.error("[Storage Addon Webhook] Error updating user_profil.stockage:", error);
100
+ // Don't fail - the addon is still recorded
101
+ }
102
+ break;
103
+ }
104
+ case "addon.deleted":
105
+ case "addon.canceled": {
106
+ // Mark addon as canceled but keep for period_end grace
107
+ const { error: cancelError } = await supabase
108
+ .from("user_global_addons")
109
+ .update({
110
+ status: "canceled",
111
+ updated_at: new Date().toISOString(),
112
+ })
113
+ .eq("stripe_subscription_item_id", stripe_subscription_item_id);
114
+ if (cancelError) {
115
+ logger.error("[Storage Addon Webhook] Error canceling addon:", cancelError);
116
+ return Response.json({ error: "Failed to cancel storage addon" }, { status: 500 });
117
+ }
118
+ logger.debug(`[Storage Addon Webhook] Storage addon ${addon_key} canceled for user ${owner_id}`);
119
+ // Recalculate total storage
120
+ try {
121
+ const limitsResult = await supabase.rpc("get_user_limits", {
122
+ owner_id_param: owner_id,
123
+ });
124
+ if (limitsResult.data) {
125
+ const limitsData = typeof limitsResult.data === "string"
126
+ ? JSON.parse(limitsResult.data)
127
+ : limitsResult.data;
128
+ const totalStorageGb = limitsData.storage_quota_gb;
129
+ await supabase.from("user_profil").upsert({
130
+ owner_id,
131
+ stockage: totalStorageGb,
132
+ updated_at: new Date().toISOString(),
133
+ }, {
134
+ onConflict: "owner_id",
135
+ });
136
+ logger.debug(`[Storage Addon Webhook] Recalculated user_profil.stockage to ${totalStorageGb} GB after addon cancellation`);
137
+ }
138
+ }
139
+ catch (error) {
140
+ logger.warn("[Storage Addon Webhook] Could not recalculate storage:", error);
141
+ }
142
+ break;
143
+ }
144
+ default:
145
+ logger.warn(`[Storage Addon Webhook] Unknown event type: ${eventType}`);
146
+ }
147
+ return Response.json({ received: true }, { status: 200 });
148
+ }
149
+ catch (error) {
150
+ logger.error("[Storage Addon Webhook] Error:", error);
151
+ return Response.json({
152
+ error: error instanceof Error ? error.message : "Webhook error",
153
+ }, { status: 400 });
154
+ }
155
+ }