@lastbrain/module-auth 2.0.19 → 2.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +134 -14
- 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/{web → components}/auth/dashboard.d.ts +1 -1
- package/dist/components/auth/dashboard.d.ts.map +1 -0
- package/dist/components/auth/dashboard.js +74 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- 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 +134 -14
- package/src/components/AccountButton.tsx +114 -90
- package/src/components/Doc.tsx +47 -9
- package/src/components/HasProfil.tsx +63 -0
- package/src/{web → components}/auth/dashboard.tsx +64 -20
- package/src/i18n/en.json +78 -8
- package/src/i18n/es.json +330 -0
- package/src/i18n/fr.json +75 -8
- package/src/index.ts +3 -1
- 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.map +0 -1
- package/dist/web/auth/dashboard.js +0 -48
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
import { getStorageUsage, StorageUsageData } from "@lastbrain/core";
|
|
4
|
+
|
|
5
|
+
// Cache simple pour éviter de recalculer trop souvent
|
|
6
|
+
const storageCache = new Map<
|
|
7
|
+
string,
|
|
8
|
+
{ data: StorageUsageData; timestamp: number }
|
|
9
|
+
>();
|
|
10
|
+
const CACHE_TTL = 30 * 1000; // 30 secondes
|
|
11
|
+
|
|
12
|
+
// Nettoyer les entrées expirées du cache
|
|
13
|
+
function cleanExpiredCache() {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
for (const [key, value] of storageCache.entries()) {
|
|
16
|
+
if (now - value.timestamp > CACHE_TTL) {
|
|
17
|
+
storageCache.delete(key);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* GET /api/admin/storage/usage
|
|
24
|
+
* Admin route: superadmin can query any user's storage usage
|
|
25
|
+
* Query params:
|
|
26
|
+
* - ownerId: required, user ID to check
|
|
27
|
+
* - forceRefresh: optional, bypass cache
|
|
28
|
+
*/
|
|
29
|
+
export async function GET(request: NextRequest) {
|
|
30
|
+
try {
|
|
31
|
+
const supabase = await getSupabaseServerClient();
|
|
32
|
+
|
|
33
|
+
// Vérifier l'authentification
|
|
34
|
+
const {
|
|
35
|
+
data: { user },
|
|
36
|
+
error: authError,
|
|
37
|
+
} = await supabase.auth.getUser();
|
|
38
|
+
|
|
39
|
+
if (authError || !user) {
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{ success: false, error: "Non authentifié" },
|
|
42
|
+
{ status: 401 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Vérifier si l'utilisateur est superadmin
|
|
47
|
+
const { data: isSuperAdmin, error: adminCheckError } = await supabase.rpc(
|
|
48
|
+
"is_superadmin",
|
|
49
|
+
{ user_id: user.id }
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (adminCheckError || !isSuperAdmin) {
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{
|
|
55
|
+
success: false,
|
|
56
|
+
error: "Accès non autorisé - réservé aux superadmins",
|
|
57
|
+
},
|
|
58
|
+
{ status: 403 }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Récupérer l'ownerId depuis les paramètres (requis pour la route admin)
|
|
63
|
+
const { searchParams } = new URL(request.url);
|
|
64
|
+
const ownerId = searchParams.get("ownerId");
|
|
65
|
+
const forceRefresh = searchParams.get("forceRefresh") === "true";
|
|
66
|
+
|
|
67
|
+
if (!ownerId) {
|
|
68
|
+
return NextResponse.json(
|
|
69
|
+
{ success: false, error: "ownerId est requis" },
|
|
70
|
+
{ status: 400 }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Nettoyer le cache avant utilisation
|
|
75
|
+
cleanExpiredCache();
|
|
76
|
+
|
|
77
|
+
// Vérifier si on a des données en cache (sauf si forceRefresh)
|
|
78
|
+
const cacheKey = `storage-${ownerId}`;
|
|
79
|
+
let shouldUseCache = false;
|
|
80
|
+
|
|
81
|
+
if (!forceRefresh) {
|
|
82
|
+
const cachedEntry = storageCache.get(cacheKey);
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
|
|
85
|
+
if (cachedEntry && now - cachedEntry.timestamp < CACHE_TTL) {
|
|
86
|
+
// Vérifier d'abord si l'allocation de stockage a changé
|
|
87
|
+
const { data: userProfile } = await supabase
|
|
88
|
+
.from("user_profil")
|
|
89
|
+
.select("stockage")
|
|
90
|
+
.eq("owner_id", ownerId)
|
|
91
|
+
.single();
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
userProfile &&
|
|
95
|
+
userProfile.stockage === cachedEntry.data.allocatedGb
|
|
96
|
+
) {
|
|
97
|
+
shouldUseCache = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Utiliser le cache si valide
|
|
103
|
+
if (shouldUseCache) {
|
|
104
|
+
const cached = storageCache.get(cacheKey);
|
|
105
|
+
if (cached) {
|
|
106
|
+
return NextResponse.json({
|
|
107
|
+
success: true,
|
|
108
|
+
data: cached.data,
|
|
109
|
+
fromCache: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Calculer les données de stockage
|
|
115
|
+
const storageData = await getStorageUsage(supabase, ownerId);
|
|
116
|
+
|
|
117
|
+
// Mettre en cache
|
|
118
|
+
storageCache.set(cacheKey, {
|
|
119
|
+
data: storageData,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return NextResponse.json({
|
|
124
|
+
success: true,
|
|
125
|
+
data: storageData,
|
|
126
|
+
fromCache: false,
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error("[Admin Storage Usage] Error:", error);
|
|
130
|
+
return NextResponse.json(
|
|
131
|
+
{
|
|
132
|
+
success: false,
|
|
133
|
+
error:
|
|
134
|
+
error instanceof Error
|
|
135
|
+
? error.message
|
|
136
|
+
: "Erreur lors du calcul du stockage",
|
|
137
|
+
},
|
|
138
|
+
{ status: 500 }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -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
|
* POST /api/admin/users/[id]/notifications
|
|
@@ -44,7 +45,7 @@ export async function POST(
|
|
|
44
45
|
.single();
|
|
45
46
|
|
|
46
47
|
if (error) {
|
|
47
|
-
|
|
48
|
+
logger.error("Error creating notification:", error);
|
|
48
49
|
return NextResponse.json(
|
|
49
50
|
{ error: "Database Error", message: error.message },
|
|
50
51
|
{ status: 500 }
|
|
@@ -56,7 +57,7 @@ export async function POST(
|
|
|
56
57
|
notification: data,
|
|
57
58
|
});
|
|
58
59
|
} catch (error) {
|
|
59
|
-
|
|
60
|
+
logger.error("Error in send notification endpoint:", error);
|
|
60
61
|
return NextResponse.json(
|
|
61
62
|
{
|
|
62
63
|
error: "Internal Server Error",
|
|
@@ -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/admin/users/[id]
|
|
@@ -27,7 +28,7 @@ export async function GET(
|
|
|
27
28
|
);
|
|
28
29
|
|
|
29
30
|
if (userError) {
|
|
30
|
-
|
|
31
|
+
logger.error("Error fetching user details:", userError);
|
|
31
32
|
return NextResponse.json(
|
|
32
33
|
{ error: "Database Error", message: userError.message },
|
|
33
34
|
{ status: 500 }
|
|
@@ -40,7 +41,7 @@ export async function GET(
|
|
|
40
41
|
|
|
41
42
|
return NextResponse.json(userDetails);
|
|
42
43
|
} catch (error) {
|
|
43
|
-
|
|
44
|
+
logger.error("Error in admin user details endpoint:", error);
|
|
44
45
|
return NextResponse.json(
|
|
45
46
|
{
|
|
46
47
|
error: "Internal Server Error",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServiceClient } from "@lastbrain/core/server";
|
|
3
|
+
import { logger } from "@lastbrain/core";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /api/admin/users/reactivate/[id]
|
|
7
|
+
* Attempt to reactivate a user by clearing the suspended flag in app metadata (service role required)
|
|
8
|
+
*/
|
|
9
|
+
export async function POST(
|
|
10
|
+
request: NextRequest,
|
|
11
|
+
context: { params: Promise<{ id: string }> }
|
|
12
|
+
) {
|
|
13
|
+
try {
|
|
14
|
+
const supabase = await getSupabaseServiceClient();
|
|
15
|
+
const { id: userId } = await context.params;
|
|
16
|
+
|
|
17
|
+
if (!userId) {
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: "User ID is required" },
|
|
20
|
+
{ status: 400 }
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Try using the Admin auth API if available
|
|
25
|
+
try {
|
|
26
|
+
// @ts-expect-error - supabase-js may expose auth.admin.updateUserById
|
|
27
|
+
if (supabase.auth?.admin?.updateUserById) {
|
|
28
|
+
// set app_metadata.suspended = false
|
|
29
|
+
const { data, error } = await (
|
|
30
|
+
supabase.auth as any
|
|
31
|
+
).admin.updateUserById(userId, {
|
|
32
|
+
app_metadata: { suspended: false },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (error) {
|
|
36
|
+
logger.error(
|
|
37
|
+
"Error reactivating user (admin.updateUserById):",
|
|
38
|
+
error
|
|
39
|
+
);
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{ error: "Database Error", message: error.message },
|
|
42
|
+
{ status: 500 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return NextResponse.json({ success: true, user: data });
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
logger.debug("auth.admin.updateUserById not available or failed:", err);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: try to update auth.users via SQL (may require service role privileges)
|
|
53
|
+
try {
|
|
54
|
+
const update = {
|
|
55
|
+
raw_app_meta_data: { suspended: false },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const { data, error } = await supabase
|
|
59
|
+
.from("users")
|
|
60
|
+
.update(update)
|
|
61
|
+
.eq("id", userId)
|
|
62
|
+
.select()
|
|
63
|
+
.single();
|
|
64
|
+
|
|
65
|
+
if (error) {
|
|
66
|
+
logger.error("Fallback reactivate user failed:", error);
|
|
67
|
+
return NextResponse.json(
|
|
68
|
+
{ error: "Database Error", message: error.message },
|
|
69
|
+
{ status: 500 }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return NextResponse.json({ success: true, user: data });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.error("Final reactivate fallback error:", err);
|
|
76
|
+
return NextResponse.json(
|
|
77
|
+
{ error: "Internal Server Error", message: String(err) },
|
|
78
|
+
{ status: 500 }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error("Error in reactivate user endpoint:", error);
|
|
83
|
+
return NextResponse.json(
|
|
84
|
+
{ error: "Internal Server Error", message: "Failed to reactivate user" },
|
|
85
|
+
{ status: 500 }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServiceClient } from "@lastbrain/core/server";
|
|
3
|
+
import { logger } from "@lastbrain/core";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /api/admin/users/suspend/[id]
|
|
7
|
+
* Attempt to suspend a user by setting a suspended flag in app metadata (service role required)
|
|
8
|
+
*/
|
|
9
|
+
export async function POST(
|
|
10
|
+
request: NextRequest,
|
|
11
|
+
context: { params: Promise<{ id: string }> }
|
|
12
|
+
) {
|
|
13
|
+
try {
|
|
14
|
+
const supabase = await getSupabaseServiceClient();
|
|
15
|
+
const { id: userId } = await context.params;
|
|
16
|
+
|
|
17
|
+
if (!userId) {
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: "User ID is required" },
|
|
20
|
+
{ status: 400 }
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Try using the Admin auth API if available
|
|
25
|
+
try {
|
|
26
|
+
// @ts-expect-error - supabase-js may expose auth.admin.updateUserById
|
|
27
|
+
if (supabase.auth?.admin?.updateUserById) {
|
|
28
|
+
// set app_metadata.suspended = true
|
|
29
|
+
const { data, error } = await (
|
|
30
|
+
supabase.auth as any
|
|
31
|
+
).admin.updateUserById(userId, {
|
|
32
|
+
app_metadata: { suspended: true },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (error) {
|
|
36
|
+
logger.error("Error suspending user (admin.updateUserById):", error);
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ error: "Database Error", message: error.message },
|
|
39
|
+
{ status: 500 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return NextResponse.json({ success: true, user: data });
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
logger.debug("auth.admin.updateUserById not available or failed:", err);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fallback: try to update auth.users via SQL (may require service role privileges)
|
|
50
|
+
try {
|
|
51
|
+
const update = {
|
|
52
|
+
raw_app_meta_data: { suspended: true },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const { data, error } = await supabase
|
|
56
|
+
.from("users")
|
|
57
|
+
.update(update)
|
|
58
|
+
.eq("id", userId)
|
|
59
|
+
.select()
|
|
60
|
+
.single();
|
|
61
|
+
|
|
62
|
+
if (error) {
|
|
63
|
+
logger.error("Fallback suspend user failed:", error);
|
|
64
|
+
return NextResponse.json(
|
|
65
|
+
{ error: "Database Error", message: error.message },
|
|
66
|
+
{ status: 500 }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return NextResponse.json({ success: true, user: data });
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.error("Final suspend fallback error:", err);
|
|
73
|
+
return NextResponse.json(
|
|
74
|
+
{ error: "Internal Server Error", message: String(err) },
|
|
75
|
+
{ status: 500 }
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.error("Error in suspend user endpoint:", error);
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: "Internal Server Error", message: "Failed to suspend user" },
|
|
82
|
+
{ status: 500 }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from "@lastbrain/core";
|
|
1
2
|
import { getSupabaseServiceClient } from "@lastbrain/core/server";
|
|
2
3
|
import { NextRequest, NextResponse } from "next/server";
|
|
3
4
|
|
|
@@ -78,7 +79,7 @@ export async function GET(request: NextRequest) {
|
|
|
78
79
|
{ status: 200 }
|
|
79
80
|
);
|
|
80
81
|
} catch (error) {
|
|
81
|
-
|
|
82
|
+
logger.error("Error fetching users by signup source:", error);
|
|
82
83
|
return NextResponse.json(
|
|
83
84
|
{ error: "Erreur interne du serveur" },
|
|
84
85
|
{ status: 500 }
|
package/src/api/admin/users.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/admin/users
|
|
@@ -29,7 +30,7 @@ export async function GET(request: NextRequest) {
|
|
|
29
30
|
);
|
|
30
31
|
|
|
31
32
|
if (usersError) {
|
|
32
|
-
|
|
33
|
+
logger.error("Error fetching users:", usersError);
|
|
33
34
|
return NextResponse.json(
|
|
34
35
|
{ error: "Database Error", message: usersError.message },
|
|
35
36
|
{ status: 500 }
|
|
@@ -37,6 +38,62 @@ export async function GET(request: NextRequest) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
// The RPC function returns the complete response with data and pagination
|
|
41
|
+
// Ensure each user has a normalized `app_metadata` field derived from
|
|
42
|
+
// `raw_app_meta_data` (RPCs may differ in the exact shape returned).
|
|
43
|
+
if (result && Array.isArray((result as any).data)) {
|
|
44
|
+
try {
|
|
45
|
+
const mapped = (result as any).data.map((u: any) => {
|
|
46
|
+
const out = { ...u };
|
|
47
|
+
|
|
48
|
+
// Try multiple possible locations for app metadata returned by different RPCs
|
|
49
|
+
const rawCandidate =
|
|
50
|
+
out.raw_app_meta_data ??
|
|
51
|
+
out.metadata ??
|
|
52
|
+
out.raw_user_meta_data ??
|
|
53
|
+
out.app_metadata ??
|
|
54
|
+
null;
|
|
55
|
+
|
|
56
|
+
// rawCandidate can be an object or a JSON string
|
|
57
|
+
let parsedRaw: any = rawCandidate;
|
|
58
|
+
if (typeof rawCandidate === "string") {
|
|
59
|
+
try {
|
|
60
|
+
parsedRaw = JSON.parse(rawCandidate);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
parsedRaw = rawCandidate;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If parsedRaw contains an `app_metadata` object, prefer it.
|
|
67
|
+
// Otherwise, if parsedRaw itself looks like metadata (contains suspended or provider keys), use it.
|
|
68
|
+
let derived: any = null;
|
|
69
|
+
if (parsedRaw && typeof parsedRaw === "object") {
|
|
70
|
+
if (
|
|
71
|
+
parsedRaw.app_metadata &&
|
|
72
|
+
typeof parsedRaw.app_metadata === "object"
|
|
73
|
+
) {
|
|
74
|
+
derived = parsedRaw.app_metadata;
|
|
75
|
+
} else {
|
|
76
|
+
// parsedRaw may already be the app metadata
|
|
77
|
+
derived = parsedRaw;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Ensure out.app_metadata is an object when possible
|
|
82
|
+
if (derived && typeof derived === "object") {
|
|
83
|
+
out.app_metadata = { ...(out.app_metadata || {}), ...derived };
|
|
84
|
+
} else if (out.app_metadata == null) {
|
|
85
|
+
out.app_metadata = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return out;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
(result as any).data = mapped;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
logger.debug("Failed to normalize app_metadata for users list:", e);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
40
97
|
return NextResponse.json(
|
|
41
98
|
result || {
|
|
42
99
|
data: [],
|
|
@@ -44,7 +101,7 @@ export async function GET(request: NextRequest) {
|
|
|
44
101
|
}
|
|
45
102
|
);
|
|
46
103
|
} catch (error) {
|
|
47
|
-
|
|
104
|
+
logger.error("Error in admin users endpoint:", error);
|
|
48
105
|
return NextResponse.json(
|
|
49
106
|
{
|
|
50
107
|
error: "Internal Server Error",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
import { sendEmailChangeConfirmation } from "../../../lib/auth-email-service";
|
|
4
|
+
import { logger } from "@lastbrain/core";
|
|
5
|
+
|
|
6
|
+
function extractLocale(request: NextRequest): string | undefined {
|
|
7
|
+
const lang = request.headers
|
|
8
|
+
.get("accept-language")
|
|
9
|
+
?.split(",")?.[0]
|
|
10
|
+
?.split("-")?.[0];
|
|
11
|
+
return lang === "fr" || lang === "en" ? lang : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function POST(request: NextRequest) {
|
|
15
|
+
try {
|
|
16
|
+
const locale = extractLocale(request);
|
|
17
|
+
const supabase = await getSupabaseServerClient();
|
|
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
|
+
const body = await request.json();
|
|
28
|
+
const newEmail = body?.newEmail as string;
|
|
29
|
+
|
|
30
|
+
if (!newEmail) {
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ error: "Nouvel email requis" },
|
|
33
|
+
{ status: 400 }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await sendEmailChangeConfirmation({
|
|
38
|
+
email: user.email || "",
|
|
39
|
+
newEmail,
|
|
40
|
+
displayName: (user.user_metadata as any)?.full_name,
|
|
41
|
+
locale,
|
|
42
|
+
ownerId: user.id,
|
|
43
|
+
redirectTo: `${request.nextUrl.origin}/api/public/callback?next=/confirm`,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return NextResponse.json({ message: "Lien de confirmation envoyé" });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logger.error("email-change error:", error);
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: "Impossible d'envoyer le lien de confirmation" },
|
|
51
|
+
{ status: 500 }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
import { sendPasswordResetEmail } 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
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_LOCALE = "fr";
|
|
16
|
+
|
|
17
|
+
export async function POST(request: NextRequest) {
|
|
18
|
+
try {
|
|
19
|
+
const locale = extractLocale(request);
|
|
20
|
+
const supabase = await getSupabaseServerClient();
|
|
21
|
+
const {
|
|
22
|
+
data: { user },
|
|
23
|
+
error,
|
|
24
|
+
} = await supabase.auth.getUser();
|
|
25
|
+
|
|
26
|
+
if (error || !user) {
|
|
27
|
+
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const resolvedLocale = locale || DEFAULT_LOCALE;
|
|
31
|
+
await sendPasswordResetEmail({
|
|
32
|
+
email: user.email || "",
|
|
33
|
+
displayName: (user.user_metadata as any)?.full_name,
|
|
34
|
+
ownerId: user.id,
|
|
35
|
+
redirectTo: `${resolveSiteUrl(request)}/${resolvedLocale}/reset-password`,
|
|
36
|
+
locale,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return NextResponse.json({ message: "Lien de réinitialisation envoyé" });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
logger.error("account reset-password error:", err);
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ error: "Impossible d'envoyer le lien de réinitialisation" },
|
|
44
|
+
{ status: 500 }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check username availability
|
|
3
|
+
* GET /api/auth/check-username?username=xxx&owner_id=xxx
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
7
|
+
import { getSupabaseServiceClient } from "@lastbrain/core/server";
|
|
8
|
+
|
|
9
|
+
export async function GET(req: NextRequest) {
|
|
10
|
+
try {
|
|
11
|
+
const { searchParams } = new URL(req.url);
|
|
12
|
+
const username = searchParams.get("username");
|
|
13
|
+
const owner_id = searchParams.get("owner_id");
|
|
14
|
+
|
|
15
|
+
if (!username) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ available: false, error: "Username required" },
|
|
18
|
+
{ status: 400 }
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const supabase = getSupabaseServiceClient();
|
|
23
|
+
|
|
24
|
+
// Check if username exists (case-insensitive), excluding current user
|
|
25
|
+
let query = supabase
|
|
26
|
+
.from("user_profil")
|
|
27
|
+
.select("username")
|
|
28
|
+
.ilike("username", username)
|
|
29
|
+
.limit(1);
|
|
30
|
+
|
|
31
|
+
if (owner_id) {
|
|
32
|
+
query = query.neq("owner_id", owner_id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { data, error } = await query;
|
|
36
|
+
|
|
37
|
+
if (error) {
|
|
38
|
+
return NextResponse.json(
|
|
39
|
+
{ available: false, error: error.message },
|
|
40
|
+
{ status: 500 }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return NextResponse.json({ available: !data || data.length === 0 });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("[check-username] Error:", error);
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ available: false, error: "Internal server error" },
|
|
49
|
+
{ status: 500 }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Establish server-side session with cookies
|
|
3
|
+
* Called after client-side setSession() to ensure HTTP-only cookies are set
|
|
4
|
+
*/
|
|
5
|
+
import { logger } from "@lastbrain/core";
|
|
6
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
7
|
+
|
|
8
|
+
export async function POST(request: Request) {
|
|
9
|
+
try {
|
|
10
|
+
const supabase = await getSupabaseServerClient();
|
|
11
|
+
|
|
12
|
+
// Verify session exists on server
|
|
13
|
+
const {
|
|
14
|
+
data: { user },
|
|
15
|
+
error: userError,
|
|
16
|
+
} = await supabase.auth.getUser();
|
|
17
|
+
|
|
18
|
+
if (userError || !user) {
|
|
19
|
+
logger.error("❌ No user session on server:", userError);
|
|
20
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Cookies are automatically set by Supabase middleware
|
|
24
|
+
return Response.json({ success: true, user: user.id });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger.debug("💥 Error establishing session:", error);
|
|
27
|
+
return Response.json(
|
|
28
|
+
{ error: "Failed to establish session" },
|
|
29
|
+
{ status: 500 }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|