@lastbrain/module-auth 2.0.27 → 2.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -7
- package/dist/api/admin/signup-stats.d.ts.map +1 -1
- package/dist/api/admin/signup-stats.js +2 -1
- package/dist/api/admin/storage/usage.d.ts +18 -0
- package/dist/api/admin/storage/usage.d.ts.map +1 -0
- package/dist/api/admin/storage/usage.js +100 -0
- package/dist/api/admin/users/[id]/notifications.d.ts.map +1 -1
- package/dist/api/admin/users/[id]/notifications.js +3 -2
- package/dist/api/admin/users/[id].d.ts.map +1 -1
- package/dist/api/admin/users/[id].js +3 -2
- package/dist/api/admin/users/reactivate/[id].d.ts +16 -0
- package/dist/api/admin/users/reactivate/[id].d.ts.map +1 -0
- package/dist/api/admin/users/reactivate/[id].js +59 -0
- package/dist/api/admin/users/suspend/[id].d.ts +16 -0
- package/dist/api/admin/users/suspend/[id].d.ts.map +1 -0
- package/dist/api/admin/users/suspend/[id].js +59 -0
- package/dist/api/admin/users-by-source.d.ts.map +1 -1
- package/dist/api/admin/users-by-source.js +2 -1
- package/dist/api/admin/users.d.ts.map +1 -1
- package/dist/api/admin/users.js +53 -2
- package/dist/api/auth/account/email-change.d.ts +7 -0
- package/dist/api/auth/account/email-change.d.ts.map +1 -0
- package/dist/api/auth/account/email-change.js +39 -0
- package/dist/api/auth/account/reset-password.d.ts +7 -0
- package/dist/api/auth/account/reset-password.d.ts.map +1 -0
- package/dist/api/auth/account/reset-password.js +36 -0
- package/dist/api/auth/check-username.d.ts +9 -0
- package/dist/api/auth/check-username.d.ts.map +1 -0
- package/dist/api/auth/check-username.js +35 -0
- package/dist/api/auth/establish-session.d.ts +2 -0
- package/dist/api/auth/establish-session.d.ts.map +1 -0
- package/dist/api/auth/establish-session.js +23 -0
- package/dist/api/auth/me.d.ts +4 -4
- package/dist/api/auth/me.d.ts.map +1 -1
- package/dist/api/auth/me.js +28 -6
- package/dist/api/auth/profile.d.ts.map +1 -1
- package/dist/api/auth/profile.js +6 -3
- package/dist/api/auth/storage/recalculate.d.ts +15 -0
- package/dist/api/auth/storage/recalculate.d.ts.map +1 -0
- package/dist/api/auth/storage/recalculate.js +68 -0
- package/dist/api/auth/storage/usage.d.ts +10 -0
- package/dist/api/auth/storage/usage.d.ts.map +1 -0
- package/dist/api/auth/storage/usage.js +86 -0
- package/dist/api/public/add-welcome-bonus.d.ts +16 -0
- package/dist/api/public/add-welcome-bonus.d.ts.map +1 -0
- package/dist/api/public/add-welcome-bonus.js +177 -0
- package/dist/api/public/callback.d.ts +3 -0
- package/dist/api/public/callback.d.ts.map +1 -0
- package/dist/api/public/callback.js +197 -0
- package/dist/api/public/reset-password.d.ts +3 -0
- package/dist/api/public/reset-password.d.ts.map +1 -0
- package/dist/api/public/reset-password.js +43 -0
- package/dist/api/public/set-session.d.ts +7 -0
- package/dist/api/public/set-session.d.ts.map +1 -0
- package/dist/api/public/set-session.js +55 -0
- package/dist/api/public/signin.d.ts.map +1 -1
- package/dist/api/public/signin.js +31 -0
- package/dist/api/public/signup.d.ts.map +1 -1
- package/dist/api/public/signup.js +38 -27
- package/dist/api/public/webhook/storage-addon.d.ts +9 -0
- package/dist/api/public/webhook/storage-addon.d.ts.map +1 -0
- package/dist/api/public/webhook/storage-addon.js +155 -0
- package/dist/api/storage.js +2 -2
- package/dist/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +126 -11
- package/dist/components/AccountButton.d.ts.map +1 -1
- package/dist/components/AccountButton.js +54 -28
- package/dist/components/Doc.d.ts.map +1 -1
- package/dist/components/Doc.js +1 -1
- package/dist/components/HasProfil.d.ts +4 -0
- package/dist/components/HasProfil.d.ts.map +1 -0
- package/dist/components/HasProfil.js +39 -0
- package/dist/components/auth/dashboard.d.ts +1 -1
- package/dist/components/auth/dashboard.d.ts.map +1 -1
- package/dist/components/auth/dashboard.js +34 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/app-branding-data.d.ts +22 -0
- package/dist/lib/app-branding-data.d.ts.map +1 -0
- package/dist/lib/app-branding-data.js +49 -0
- package/dist/lib/auth-email-service.d.ts +57 -0
- package/dist/lib/auth-email-service.d.ts.map +1 -0
- package/dist/lib/auth-email-service.js +382 -0
- package/dist/lib/auth-email-templates.d.ts +2 -0
- package/dist/lib/auth-email-templates.d.ts.map +1 -0
- package/dist/lib/auth-email-templates.js +1 -0
- package/dist/lib/site-url.d.ts +3 -0
- package/dist/lib/site-url.d.ts.map +1 -0
- package/dist/lib/site-url.js +11 -0
- package/dist/sitemap/manifest.d.ts +9 -0
- package/dist/sitemap/manifest.d.ts.map +1 -0
- package/dist/sitemap/manifest.js +14 -0
- package/dist/web/admin/signup-stats.js +3 -3
- package/dist/web/admin/user-detail.d.ts.map +1 -1
- package/dist/web/admin/user-detail.js +135 -14
- package/dist/web/admin/users-by-signup-source.js +2 -2
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +26 -7
- package/dist/web/auth/folder.d.ts.map +1 -1
- package/dist/web/auth/folder.js +4 -3
- package/dist/web/auth/profile.d.ts.map +1 -1
- package/dist/web/auth/profile.js +132 -13
- package/dist/web/auth/reglage.d.ts.map +1 -1
- package/dist/web/auth/reglage.js +15 -8
- package/dist/web/public/ResetPassword.d.ts.map +1 -1
- package/dist/web/public/ResetPassword.js +172 -2
- package/dist/web/public/SignInPage.d.ts.map +1 -1
- package/dist/web/public/SignInPage.js +39 -3
- package/dist/web/public/SignUpPage.d.ts.map +1 -1
- package/dist/web/public/SignUpPage.js +7 -2
- package/dist/web/public/auth-code-error.d.ts +2 -0
- package/dist/web/public/auth-code-error.d.ts.map +1 -0
- package/dist/web/public/auth-code-error.js +14 -0
- package/dist/web/public/confirm.d.ts +2 -0
- package/dist/web/public/confirm.d.ts.map +1 -0
- package/dist/web/public/confirm.js +157 -0
- package/package.json +10 -5
- package/src/api/admin/signup-stats.ts +2 -1
- package/src/api/admin/storage/usage.ts +141 -0
- package/src/api/admin/users/[id]/notifications.ts +3 -2
- package/src/api/admin/users/[id].ts +3 -2
- package/src/api/admin/users/reactivate/[id].ts +88 -0
- package/src/api/admin/users/suspend/[id].ts +85 -0
- package/src/api/admin/users-by-source.ts +2 -1
- package/src/api/admin/users.ts +59 -2
- package/src/api/auth/account/email-change.ts +54 -0
- package/src/api/auth/account/reset-password.ts +47 -0
- package/src/api/auth/check-username.ts +52 -0
- package/src/api/auth/establish-session.ts +32 -0
- package/src/api/auth/me.ts +29 -7
- package/src/api/auth/profile.ts +6 -2
- package/src/api/auth/storage/recalculate.ts +108 -0
- package/src/api/auth/storage/usage.ts +113 -0
- package/src/api/public/add-welcome-bonus.ts +229 -0
- package/src/api/public/callback.ts +307 -0
- package/src/api/public/reset-password.ts +52 -0
- package/src/api/public/set-session.ts +73 -0
- package/src/api/public/signin.ts +36 -0
- package/src/api/public/signup.ts +44 -37
- package/src/api/public/webhook/storage-addon.ts +267 -0
- package/src/api/storage.ts +2 -2
- package/src/auth.build.config.ts +126 -11
- package/src/components/AccountButton.tsx +114 -90
- package/src/components/Doc.tsx +47 -9
- package/src/components/HasProfil.tsx +63 -0
- package/src/components/auth/dashboard.tsx +54 -13
- package/src/i18n/en.json +76 -8
- package/src/i18n/es.json +330 -0
- package/src/i18n/fr.json +74 -8
- package/src/index.ts +2 -0
- package/src/lib/app-branding-data.ts +90 -0
- package/src/lib/auth-email-service.ts +508 -0
- package/src/lib/auth-email-templates.ts +5 -0
- package/src/lib/site-url.ts +17 -0
- package/src/sitemap/manifest.ts +26 -0
- package/src/web/admin/signup-stats.tsx +3 -3
- package/src/web/admin/user-detail.tsx +314 -15
- package/src/web/admin/users-by-signup-source.tsx +2 -2
- package/src/web/admin/users.tsx +50 -14
- package/src/web/auth/folder.tsx +23 -5
- package/src/web/auth/profile.tsx +227 -13
- package/src/web/auth/reglage.tsx +55 -24
- package/src/web/public/ResetPassword.tsx +301 -1
- package/src/web/public/SignInPage.tsx +43 -3
- package/src/web/public/SignUpPage.tsx +14 -5
- package/src/web/public/auth-code-error.tsx +49 -0
- package/src/web/public/confirm.tsx +195 -0
- package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +3 -1
- package/supabase/migrations/20251127100000_rename_body_to_message.sql +8 -3
- package/supabase/migrations/20260120150001_add_user_storage.sql +105 -0
- package/supabase/migrations/20260122131200_add_global_addons_system.sql +305 -0
- package/supabase/migrations/20260123100000_enable_vector_extension.sql +9 -0
- package/supabase/migrations/20260123140000_module_auth_fix_get_user_limits_null.sql +93 -0
- package/supabase/migrations/20260123145000_module_auth_fix_circular_storage_base.sql +89 -0
- package/supabase/migrations/20260129400000_add_username_to_user_profil.sql +33 -0
- package/dist/web/auth/dashboard.d.ts +0 -2
- package/dist/web/auth/dashboard.d.ts.map +0 -1
- package/dist/web/auth/dashboard.js +0 -48
|
@@ -0,0 +1,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
|
+
}
|
package/src/api/public/signin.ts
CHANGED
|
@@ -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
|
}
|
package/src/api/public/signup.ts
CHANGED
|
@@ -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
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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",
|
|
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:
|
|
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
|
-
|
|
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
|
|
94
|
-
message:
|
|
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
|
-
|
|
107
|
+
logger.error("Signup error:", error);
|
|
101
108
|
return NextResponse.json(
|
|
102
109
|
{ error: "Erreur interne du serveur" },
|
|
103
110
|
{ status: 500 }
|