@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
package/src/api/auth/me.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
1
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
2
2
|
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
import { logger } from "@lastbrain/core";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* GET /api/auth/me
|
|
6
7
|
* Returns the current authenticated user and their profile
|
|
7
8
|
*/
|
|
8
|
-
export async function GET() {
|
|
9
|
+
export async function GET(request: NextRequest) {
|
|
9
10
|
try {
|
|
10
11
|
const supabase = await getSupabaseServerClient();
|
|
11
12
|
|
|
@@ -19,20 +20,41 @@ export async function GET() {
|
|
|
19
20
|
const { data: profile } = await supabase
|
|
20
21
|
.from("user_profil")
|
|
21
22
|
.select("*")
|
|
22
|
-
.eq("owner_id", user
|
|
23
|
+
.eq("owner_id", user?.id)
|
|
23
24
|
.single();
|
|
24
25
|
|
|
25
26
|
// Profile might not exist yet, that's OK
|
|
26
27
|
const userData = {
|
|
27
|
-
id: user
|
|
28
|
-
email: user
|
|
29
|
-
created_at: user
|
|
28
|
+
id: user?.id,
|
|
29
|
+
email: user?.email,
|
|
30
|
+
created_at: user?.created_at,
|
|
30
31
|
profile: profile || null,
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
// Si le client a envoyé un cookie NEXT_LOCALE, synchroniser sa valeur
|
|
35
|
+
// avec la langue du profil (si présente). Ne rien créer si le cookie
|
|
36
|
+
// est absent. Si la langue diffère, renvoyer une réponse JSON indiquant
|
|
37
|
+
// le changement de langue et définir le cookie côté serveur. Le client
|
|
38
|
+
// effectuera ensuite la redirection côté client pour éviter que fetch
|
|
39
|
+
// suive automatiquement un redirect 302.
|
|
40
|
+
try {
|
|
41
|
+
// Return localeUpdated to let the client/realtime decide what to do.
|
|
42
|
+
const cookieLang = request.cookies.get("NEXT_LOCALE")?.value;
|
|
43
|
+
const profileLang = profile?.language;
|
|
44
|
+
if (cookieLang && profileLang && profileLang !== cookieLang) {
|
|
45
|
+
return NextResponse.json({
|
|
46
|
+
data: userData,
|
|
47
|
+
localeUpdated: true,
|
|
48
|
+
profileLang,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
logger.debug("me: locale sync error", e);
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
return NextResponse.json({ data: userData });
|
|
34
56
|
} catch (error) {
|
|
35
|
-
|
|
57
|
+
logger.error("Error fetching user:", error);
|
|
36
58
|
return NextResponse.json(
|
|
37
59
|
{ error: "Internal Server Error", message: "Failed to fetch user data" },
|
|
38
60
|
{ status: 500 }
|
package/src/api/auth/profile.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
import { logger } from "@lastbrain/core";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* GET /api/auth/profile
|
|
@@ -30,7 +31,7 @@ export async function GET() {
|
|
|
30
31
|
|
|
31
32
|
return NextResponse.json({ data: profile || null });
|
|
32
33
|
} catch (error) {
|
|
33
|
-
|
|
34
|
+
logger.error("Error fetching profile:", error);
|
|
34
35
|
return NextResponse.json(
|
|
35
36
|
{ error: "Internal Server Error", message: "Failed to fetch profile" },
|
|
36
37
|
{ status: 500 }
|
|
@@ -63,6 +64,7 @@ export async function PUT(request: NextRequest) {
|
|
|
63
64
|
location,
|
|
64
65
|
language,
|
|
65
66
|
timezone,
|
|
67
|
+
username,
|
|
66
68
|
preferences,
|
|
67
69
|
} = body;
|
|
68
70
|
|
|
@@ -90,6 +92,7 @@ export async function PUT(request: NextRequest) {
|
|
|
90
92
|
language,
|
|
91
93
|
timezone,
|
|
92
94
|
preferences,
|
|
95
|
+
username,
|
|
93
96
|
})
|
|
94
97
|
.eq("owner_id", user!.id)
|
|
95
98
|
.select()
|
|
@@ -123,9 +126,10 @@ export async function PUT(request: NextRequest) {
|
|
|
123
126
|
);
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
// Indiquer au client si la langue a été modifiée (sans toucher aux cookies)
|
|
126
130
|
return NextResponse.json({ data: result.data });
|
|
127
131
|
} catch (error) {
|
|
128
|
-
|
|
132
|
+
logger.error("Error updating profile:", error);
|
|
129
133
|
return NextResponse.json(
|
|
130
134
|
{ error: "Internal Server Error", message: "Failed to update profile" },
|
|
131
135
|
{ status: 500 }
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/storage/recalculate
|
|
3
|
+
* Force recalculation of user storage quota from addons
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
7
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
8
|
+
import { logger } from "@lastbrain/core";
|
|
9
|
+
|
|
10
|
+
export const runtime = "nodejs";
|
|
11
|
+
export const dynamic = "force-dynamic";
|
|
12
|
+
|
|
13
|
+
export async function POST(request: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
const supabase = await getSupabaseServerClient();
|
|
16
|
+
|
|
17
|
+
// Get authenticated user
|
|
18
|
+
const {
|
|
19
|
+
data: { user },
|
|
20
|
+
error: authError,
|
|
21
|
+
} = await supabase.auth.getUser();
|
|
22
|
+
|
|
23
|
+
if (authError || !user) {
|
|
24
|
+
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
logger.debug(`[Storage Recalculate] Recalculating for user ${user.id}`);
|
|
28
|
+
|
|
29
|
+
// Call get_user_limits RPC
|
|
30
|
+
const { data: limitsData, error: limitsError } = await supabase.rpc(
|
|
31
|
+
"get_user_limits",
|
|
32
|
+
{
|
|
33
|
+
owner_id_param: user.id,
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (limitsError) {
|
|
38
|
+
logger.error(
|
|
39
|
+
"[Storage Recalculate] Error calling get_user_limits:",
|
|
40
|
+
limitsError
|
|
41
|
+
);
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ error: "Erreur lors du calcul des limites" },
|
|
44
|
+
{ status: 500 }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
logger.debug(`[Storage Recalculate] get_user_limits result:`, limitsData);
|
|
49
|
+
|
|
50
|
+
// Parse result (could be string or object)
|
|
51
|
+
let parsedLimits = limitsData;
|
|
52
|
+
if (typeof limitsData === "string") {
|
|
53
|
+
try {
|
|
54
|
+
parsedLimits = JSON.parse(limitsData);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
logger.error("[Storage Recalculate] Failed to parse limits data:", e);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const totalStorageGb = parsedLimits?.storage_quota_gb;
|
|
61
|
+
|
|
62
|
+
if (totalStorageGb !== null && totalStorageGb !== undefined) {
|
|
63
|
+
// Update user_profil
|
|
64
|
+
const { error: updateError } = await supabase.from("user_profil").upsert(
|
|
65
|
+
{
|
|
66
|
+
owner_id: user.id,
|
|
67
|
+
stockage: totalStorageGb,
|
|
68
|
+
updated_at: new Date().toISOString(),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
onConflict: "owner_id",
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (updateError) {
|
|
76
|
+
logger.error(
|
|
77
|
+
"[Storage Recalculate] Error updating user_profil:",
|
|
78
|
+
updateError
|
|
79
|
+
);
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: "Erreur lors de la mise à jour" },
|
|
82
|
+
{ status: 500 }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logger.info(
|
|
87
|
+
`[Storage Recalculate] Updated user_profil.stockage to ${totalStorageGb} GB for user ${user.id}`
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return NextResponse.json({
|
|
91
|
+
success: true,
|
|
92
|
+
storage_quota_gb: totalStorageGb,
|
|
93
|
+
breakdown: parsedLimits?.storage_breakdown,
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
logger.error(
|
|
97
|
+
"[Storage Recalculate] get_user_limits returned null for storage_quota_gb"
|
|
98
|
+
);
|
|
99
|
+
return NextResponse.json({ error: "Calcul invalide" }, { status: 500 });
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logger.error("[Storage Recalculate] Error:", error);
|
|
103
|
+
return NextResponse.json(
|
|
104
|
+
{ error: "Erreur lors du recalcul" },
|
|
105
|
+
{ status: 500 }
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
import { getStorageUsage, StorageUsageData } from "@lastbrain/core";
|
|
4
|
+
|
|
5
|
+
// Cache pour éviter les calculs répétés
|
|
6
|
+
interface CacheEntry {
|
|
7
|
+
data: StorageUsageData;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const storageCache = new Map<string, CacheEntry>();
|
|
12
|
+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes en millisecondes
|
|
13
|
+
|
|
14
|
+
// Nettoyer le cache périodiquement
|
|
15
|
+
function cleanExpiredCache() {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
for (const [key, entry] of storageCache.entries()) {
|
|
18
|
+
if (now - entry.timestamp > CACHE_TTL) {
|
|
19
|
+
storageCache.delete(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function GET(request: NextRequest) {
|
|
25
|
+
try {
|
|
26
|
+
const supabase = await getSupabaseServerClient();
|
|
27
|
+
|
|
28
|
+
// Vérifier l'authentification
|
|
29
|
+
const {
|
|
30
|
+
data: { user },
|
|
31
|
+
error: authError,
|
|
32
|
+
} = await supabase.auth.getUser();
|
|
33
|
+
if (authError || !user) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ success: false, error: "Non authentifié" },
|
|
36
|
+
{ status: 401 }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Route auth: uniquement les données de l'utilisateur connecté
|
|
41
|
+
const ownerId = user.id;
|
|
42
|
+
const { searchParams } = new URL(request.url);
|
|
43
|
+
const forceRefresh = searchParams.get("forceRefresh") === "true";
|
|
44
|
+
|
|
45
|
+
// Nettoyer le cache avant utilisation
|
|
46
|
+
cleanExpiredCache();
|
|
47
|
+
|
|
48
|
+
// Vérifier si on a des données en cache (sauf si forceRefresh)
|
|
49
|
+
const cacheKey = `storage-${ownerId}`;
|
|
50
|
+
let shouldUseCache = false;
|
|
51
|
+
|
|
52
|
+
if (!forceRefresh) {
|
|
53
|
+
const cachedEntry = storageCache.get(cacheKey);
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
|
|
56
|
+
if (cachedEntry && now - cachedEntry.timestamp < CACHE_TTL) {
|
|
57
|
+
// Vérifier d'abord si l'allocation de stockage a changé
|
|
58
|
+
// Récupérer juste l'allocation depuis la base (requête légère)
|
|
59
|
+
const { data: userProfile } = await supabase
|
|
60
|
+
.from("user_profil")
|
|
61
|
+
.select("stockage")
|
|
62
|
+
.eq("owner_id", ownerId)
|
|
63
|
+
.single();
|
|
64
|
+
|
|
65
|
+
const currentAllocatedGb = userProfile?.stockage || 2.0;
|
|
66
|
+
const cachedAllocatedGb = cachedEntry.data.allocatedGb;
|
|
67
|
+
|
|
68
|
+
// Si l'allocation n'a pas changé, utiliser le cache
|
|
69
|
+
if (currentAllocatedGb === cachedAllocatedGb) {
|
|
70
|
+
shouldUseCache = true;
|
|
71
|
+
} else {
|
|
72
|
+
// L'allocation a changé, invalider le cache
|
|
73
|
+
storageCache.delete(cacheKey);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (shouldUseCache && cachedEntry) {
|
|
78
|
+
// Retourner les données depuis le cache
|
|
79
|
+
return NextResponse.json({
|
|
80
|
+
success: true,
|
|
81
|
+
data: cachedEntry.data,
|
|
82
|
+
cached: true,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// Forcer la suppression du cache pour cet utilisateur
|
|
87
|
+
storageCache.delete(cacheKey);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Récupérer les données d'usage de stockage
|
|
91
|
+
const storageData = await getStorageUsage(supabase, ownerId);
|
|
92
|
+
|
|
93
|
+
// Mettre en cache
|
|
94
|
+
storageCache.set(cacheKey, {
|
|
95
|
+
data: storageData,
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return NextResponse.json({
|
|
100
|
+
success: true,
|
|
101
|
+
data: storageData,
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error("Erreur API storage usage:", error);
|
|
105
|
+
return NextResponse.json(
|
|
106
|
+
{
|
|
107
|
+
success: false,
|
|
108
|
+
error: error instanceof Error ? error.message : "Erreur serveur",
|
|
109
|
+
},
|
|
110
|
+
{ status: 500 }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
getSupabaseServerClient,
|
|
4
|
+
getSupabaseServiceClient,
|
|
5
|
+
} from "@lastbrain/core/server";
|
|
6
|
+
import { sendWelcomeOnboardingEmail } from "../../lib/auth-email-service";
|
|
7
|
+
import { logger } from "@lastbrain/core";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/public/add-welcome-bonus
|
|
11
|
+
* Adds welcome bonus tokens and notifications after email confirmation
|
|
12
|
+
* Called from the confirm page after successful session establishment
|
|
13
|
+
*/
|
|
14
|
+
export async function POST(request: NextRequest) {
|
|
15
|
+
try {
|
|
16
|
+
// Get authenticated user from cookies
|
|
17
|
+
const supabase = await getSupabaseServerClient();
|
|
18
|
+
const {
|
|
19
|
+
data: { user },
|
|
20
|
+
error: authError,
|
|
21
|
+
} = await supabase.auth.getUser();
|
|
22
|
+
|
|
23
|
+
if (authError || !user?.id) {
|
|
24
|
+
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Use service client to bypass RLS for system operations
|
|
28
|
+
const serviceClient = await getSupabaseServiceClient();
|
|
29
|
+
|
|
30
|
+
logger.debug(
|
|
31
|
+
`[add-welcome-bonus] Starting for user ${user.id} (${user.email})`
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Check if signup bonus is enabled
|
|
35
|
+
const ENABLE_SIGNUP_BONUS =
|
|
36
|
+
process.env.NEXT_PUBLIC_ENABLE_SIGNUP_BONUS === "true";
|
|
37
|
+
const SIGNUP_BONUS_AMOUNT_USD = parseFloat(
|
|
38
|
+
process.env.NEXT_PUBLIC_SIGNUP_BONUS_AMOUNT_USD || "1.0"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (!ENABLE_SIGNUP_BONUS) {
|
|
42
|
+
logger.debug(
|
|
43
|
+
`[add-welcome-bonus] Signup bonus disabled for user ${user.id}`
|
|
44
|
+
);
|
|
45
|
+
return NextResponse.json({
|
|
46
|
+
message: "Signup bonus disabled",
|
|
47
|
+
alreadyExists: false,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if we already added bonus for this user
|
|
52
|
+
const { data: existingBonus } = await serviceClient
|
|
53
|
+
.from("user_token_ledger")
|
|
54
|
+
.select("id")
|
|
55
|
+
.eq("owner_id", user.id)
|
|
56
|
+
.eq("type", "signup_bonus")
|
|
57
|
+
.single();
|
|
58
|
+
|
|
59
|
+
// If bonus already exists, skip
|
|
60
|
+
if (existingBonus) {
|
|
61
|
+
logger.debug(`⏭️ Bonus already exists for user ${user.id}, skipping...`);
|
|
62
|
+
return NextResponse.json({
|
|
63
|
+
message: "Bonus already added",
|
|
64
|
+
alreadyExists: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add welcome bonus using wallet system
|
|
69
|
+
const MARGIN_TARGET = 0.6;
|
|
70
|
+
const providerBudgetUsd = SIGNUP_BONUS_AMOUNT_USD * (1 - MARGIN_TARGET); // 40%
|
|
71
|
+
|
|
72
|
+
// Insert into ledger with USD values
|
|
73
|
+
const { error: ledgerError } = await serviceClient
|
|
74
|
+
.from("user_token_ledger")
|
|
75
|
+
.insert({
|
|
76
|
+
owner_id: user.id,
|
|
77
|
+
type: "signup_bonus",
|
|
78
|
+
amount: 0, // Tokens deprecated
|
|
79
|
+
sell_value_added_usd: SIGNUP_BONUS_AMOUNT_USD,
|
|
80
|
+
provider_budget_added_usd: providerBudgetUsd,
|
|
81
|
+
meta: {
|
|
82
|
+
reason: "signup_welcome_bonus",
|
|
83
|
+
provider: "system",
|
|
84
|
+
},
|
|
85
|
+
created_by: null,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (ledgerError) {
|
|
89
|
+
logger.error("Error adding welcome bonus to ledger:", ledgerError);
|
|
90
|
+
return NextResponse.json(
|
|
91
|
+
{ error: "Failed to add bonus" },
|
|
92
|
+
{ status: 500 }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Update wallet table
|
|
97
|
+
const { data: currentWallet } = await serviceClient
|
|
98
|
+
.from("user_token_wallet")
|
|
99
|
+
.select("wallet_provider_budget_usd, wallet_sell_value_usd")
|
|
100
|
+
.eq("user_id", user.id)
|
|
101
|
+
.single();
|
|
102
|
+
|
|
103
|
+
const newProviderBudget =
|
|
104
|
+
(currentWallet?.wallet_provider_budget_usd || 0) + providerBudgetUsd;
|
|
105
|
+
const newSellValue =
|
|
106
|
+
(currentWallet?.wallet_sell_value_usd || 0) + SIGNUP_BONUS_AMOUNT_USD;
|
|
107
|
+
|
|
108
|
+
const { error: walletError } = await serviceClient
|
|
109
|
+
.from("user_token_wallet")
|
|
110
|
+
.upsert(
|
|
111
|
+
{
|
|
112
|
+
user_id: user.id,
|
|
113
|
+
wallet_provider_budget_usd: newProviderBudget,
|
|
114
|
+
wallet_sell_value_usd: newSellValue,
|
|
115
|
+
updated_at: new Date().toISOString(),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
onConflict: "user_id",
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (walletError) {
|
|
123
|
+
logger.error("Error updating wallet for welcome bonus:", walletError);
|
|
124
|
+
return NextResponse.json(
|
|
125
|
+
{ error: "Failed to update wallet" },
|
|
126
|
+
{ status: 500 }
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
logger.debug(
|
|
131
|
+
`✅ Added $${SIGNUP_BONUS_AMOUNT_USD} welcome bonus to user ${user.id}`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Determine locale: prefer cookie set by the client (NEXT_LOCALE),
|
|
135
|
+
// then user metadata, then Accept-Language header, fallback to 'en'.
|
|
136
|
+
let locale = "en";
|
|
137
|
+
try {
|
|
138
|
+
const cookieLocale = request.cookies.get("NEXT_LOCALE")?.value;
|
|
139
|
+
if (cookieLocale) {
|
|
140
|
+
locale = cookieLocale;
|
|
141
|
+
} else if (user.user_metadata?.locale) {
|
|
142
|
+
locale = user.user_metadata.locale as string;
|
|
143
|
+
} else {
|
|
144
|
+
const accept = request.headers.get("accept-language");
|
|
145
|
+
if (accept) {
|
|
146
|
+
// take first language tag, e.g. 'fr-FR,fr;q=0.9' -> 'fr'
|
|
147
|
+
const first = accept.split(",")[0].split("-")[0];
|
|
148
|
+
if (first) locale = first;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
// if anything goes wrong, keep default 'en'
|
|
153
|
+
logger.warn(
|
|
154
|
+
"Could not determine locale from request, using default 'en'",
|
|
155
|
+
e
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add welcome notifications only if bonus enabled
|
|
160
|
+
if (ENABLE_SIGNUP_BONUS && SIGNUP_BONUS_AMOUNT_USD > 0) {
|
|
161
|
+
const notifications = [
|
|
162
|
+
{
|
|
163
|
+
title: locale === "fr" ? "Bienvenue ! 👋" : "Welcome! 👋",
|
|
164
|
+
body:
|
|
165
|
+
locale === "fr"
|
|
166
|
+
? "Merci de vous être inscrit ! Explorez toutes les fonctionnalités disponibles."
|
|
167
|
+
: "Thank you for signing up! Explore all available features.",
|
|
168
|
+
type: "primary",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
title:
|
|
172
|
+
locale === "fr"
|
|
173
|
+
? `🎁 Cadeau de bienvenue : $${SIGNUP_BONUS_AMOUNT_USD}`
|
|
174
|
+
: `🎁 Welcome gift: $${SIGNUP_BONUS_AMOUNT_USD}`,
|
|
175
|
+
body:
|
|
176
|
+
locale === "fr"
|
|
177
|
+
? `Nous vous offrons $${SIGNUP_BONUS_AMOUNT_USD} de crédit IA pour démarrer. Utilisez-les pour accéder à nos services premium.`
|
|
178
|
+
: `We're giving you $${SIGNUP_BONUS_AMOUNT_USD} AI credits to get started. Use them to access our premium services.`,
|
|
179
|
+
type: "success",
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
for (const notification of notifications) {
|
|
184
|
+
const { error: notifError } = await serviceClient
|
|
185
|
+
.from("user_notifications")
|
|
186
|
+
.insert({
|
|
187
|
+
owner_id: user.id,
|
|
188
|
+
title: notification.title,
|
|
189
|
+
body: notification.body,
|
|
190
|
+
type: notification.type,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (notifError) {
|
|
194
|
+
logger.error("Error creating notification:", notifError);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
logger.debug(`✅ Created welcome notifications for user ${user.id}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Send welcome onboarding email with bonus amount if enabled
|
|
202
|
+
try {
|
|
203
|
+
await sendWelcomeOnboardingEmail({
|
|
204
|
+
email: user.email || "",
|
|
205
|
+
displayName: (user.user_metadata?.full_name as string) || "user",
|
|
206
|
+
locale,
|
|
207
|
+
ownerId: user.id,
|
|
208
|
+
bonusAmountUsd: ENABLE_SIGNUP_BONUS
|
|
209
|
+
? SIGNUP_BONUS_AMOUNT_USD
|
|
210
|
+
: undefined,
|
|
211
|
+
});
|
|
212
|
+
logger.debug(`📧 Sent welcome onboarding email to ${user.email}`);
|
|
213
|
+
} catch (emailError) {
|
|
214
|
+
logger.warn("Failed to send welcome email (non-blocking):", emailError);
|
|
215
|
+
// Don't fail the response if email fails
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return NextResponse.json({
|
|
219
|
+
success: true,
|
|
220
|
+
message: "Welcome bonus added successfully",
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
logger.error("Error in add-welcome-bonus:", error);
|
|
224
|
+
return NextResponse.json(
|
|
225
|
+
{ error: "Internal server error" },
|
|
226
|
+
{ status: 500 }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|