@lastbrain/module-auth 0.1.6 → 0.1.9
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/dist/api/admin/users/[id]/notifications.d.ts +16 -0
- package/dist/api/admin/users/[id]/notifications.d.ts.map +1 -0
- package/dist/api/admin/users/[id]/notifications.js +47 -0
- package/dist/api/admin/users/[id].d.ts +11 -0
- package/dist/api/admin/users/[id].d.ts.map +1 -0
- package/dist/api/admin/users/[id].js +32 -0
- package/dist/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +46 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/web/admin/user-detail.d.ts +6 -0
- package/dist/web/admin/user-detail.d.ts.map +1 -0
- package/dist/web/admin/user-detail.js +106 -0
- package/dist/web/admin/users/[id].d.ts +8 -0
- package/dist/web/admin/users/[id].d.ts.map +1 -0
- package/dist/web/admin/users/[id].js +6 -0
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +37 -42
- package/package.json +1 -1
- package/src/api/admin/users/[id]/notifications.ts +68 -0
- package/src/api/admin/users/[id].ts +52 -0
- package/src/auth.build.config.ts +46 -0
- package/src/index.ts +1 -0
- package/src/web/admin/user-detail.tsx +348 -0
- package/src/web/admin/users/[id].tsx +12 -0
- package/src/web/admin/users.tsx +57 -78
- package/supabase/migrations/20251124000001_add_get_admin_user_details.sql +63 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* POST /api/admin/users/[id]/notifications
|
|
4
|
+
* Send a notification to a specific user (superadmin only)
|
|
5
|
+
*/
|
|
6
|
+
export declare function POST(request: NextRequest, context: {
|
|
7
|
+
params: Promise<{
|
|
8
|
+
id: string;
|
|
9
|
+
}>;
|
|
10
|
+
}): Promise<NextResponse<{
|
|
11
|
+
error: string;
|
|
12
|
+
}> | NextResponse<{
|
|
13
|
+
success: boolean;
|
|
14
|
+
notification: any;
|
|
15
|
+
}>>;
|
|
16
|
+
//# sourceMappingURL=notifications.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../../../../src/api/admin/users/[id]/notifications.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD;;;GAGG;AACH,wBAAsB,IAAI,CACxB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE;;;;;IA0D7C"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
/**
|
|
4
|
+
* POST /api/admin/users/[id]/notifications
|
|
5
|
+
* Send a notification to a specific user (superadmin only)
|
|
6
|
+
*/
|
|
7
|
+
export async function POST(request, context) {
|
|
8
|
+
try {
|
|
9
|
+
const supabase = await getSupabaseServerClient();
|
|
10
|
+
const { id: userId } = await context.params;
|
|
11
|
+
if (!userId) {
|
|
12
|
+
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
|
|
13
|
+
}
|
|
14
|
+
const body = await request.json();
|
|
15
|
+
const { title, message, type = "info" } = body;
|
|
16
|
+
if (!title?.trim() || !message?.trim()) {
|
|
17
|
+
return NextResponse.json({ error: "Title and message are required" }, { status: 400 });
|
|
18
|
+
}
|
|
19
|
+
// Insérer la notification dans la base de données
|
|
20
|
+
const { data, error } = await supabase
|
|
21
|
+
.from("user_notifications")
|
|
22
|
+
.insert({
|
|
23
|
+
owner_id: userId,
|
|
24
|
+
title: title.trim(),
|
|
25
|
+
body: message.trim(),
|
|
26
|
+
type: type,
|
|
27
|
+
read: false,
|
|
28
|
+
})
|
|
29
|
+
.select()
|
|
30
|
+
.single();
|
|
31
|
+
if (error) {
|
|
32
|
+
console.error("Error creating notification:", error);
|
|
33
|
+
return NextResponse.json({ error: "Database Error", message: error.message }, { status: 500 });
|
|
34
|
+
}
|
|
35
|
+
return NextResponse.json({
|
|
36
|
+
success: true,
|
|
37
|
+
notification: data,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error("Error in send notification endpoint:", error);
|
|
42
|
+
return NextResponse.json({
|
|
43
|
+
error: "Internal Server Error",
|
|
44
|
+
message: "Failed to send notification",
|
|
45
|
+
}, { status: 500 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* GET /api/admin/users/[id]
|
|
4
|
+
* Returns user details by ID (superadmin only)
|
|
5
|
+
*/
|
|
6
|
+
export declare function GET(request: NextRequest, context: {
|
|
7
|
+
params: Promise<{
|
|
8
|
+
id: string;
|
|
9
|
+
}>;
|
|
10
|
+
}): Promise<NextResponse<any>>;
|
|
11
|
+
//# sourceMappingURL=%5Bid%5D.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"[id].d.ts","sourceRoot":"","sources":["../../../../src/api/admin/users/[id].ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD;;;GAGG;AACH,wBAAsB,GAAG,CACvB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,8BA0C7C"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
/**
|
|
4
|
+
* GET /api/admin/users/[id]
|
|
5
|
+
* Returns user details by ID (superadmin only)
|
|
6
|
+
*/
|
|
7
|
+
export async function GET(request, context) {
|
|
8
|
+
try {
|
|
9
|
+
const supabase = await getSupabaseServerClient();
|
|
10
|
+
const { id: userId } = await context.params;
|
|
11
|
+
if (!userId) {
|
|
12
|
+
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
|
|
13
|
+
}
|
|
14
|
+
// Utiliser la fonction RPC pour récupérer les détails de l'utilisateur
|
|
15
|
+
const { data: userDetails, error: userError } = await supabase.rpc("get_admin_user_details", { user_id: userId });
|
|
16
|
+
if (userError) {
|
|
17
|
+
console.error("Error fetching user details:", userError);
|
|
18
|
+
return NextResponse.json({ error: "Database Error", message: userError.message }, { status: 500 });
|
|
19
|
+
}
|
|
20
|
+
if (!userDetails) {
|
|
21
|
+
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
|
22
|
+
}
|
|
23
|
+
return NextResponse.json(userDetails);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error("Error in admin user details endpoint:", error);
|
|
27
|
+
return NextResponse.json({
|
|
28
|
+
error: "Internal Server Error",
|
|
29
|
+
message: "Failed to fetch user details",
|
|
30
|
+
}, { status: 500 });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.build.config.d.ts","sourceRoot":"","sources":["../src/auth.build.config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,QAAA,MAAM,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"auth.build.config.d.ts","sourceRoot":"","sources":["../src/auth.build.config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,QAAA,MAAM,eAAe,EAAE,iBAqMtB,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -36,6 +36,11 @@ const authBuildConfig = {
|
|
|
36
36
|
path: "/users",
|
|
37
37
|
componentExport: "AdminUsersPage",
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
section: "admin",
|
|
41
|
+
path: "/users/[id]",
|
|
42
|
+
componentExport: "UserPage",
|
|
43
|
+
},
|
|
39
44
|
],
|
|
40
45
|
apis: [
|
|
41
46
|
{
|
|
@@ -80,6 +85,20 @@ const authBuildConfig = {
|
|
|
80
85
|
entryPoint: "api/admin/users",
|
|
81
86
|
authRequired: true,
|
|
82
87
|
},
|
|
88
|
+
{
|
|
89
|
+
method: "GET",
|
|
90
|
+
path: "/api/admin/users/[id]",
|
|
91
|
+
handlerExport: "GET",
|
|
92
|
+
entryPoint: "api/admin/users/[id]",
|
|
93
|
+
authRequired: true,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
method: "POST",
|
|
97
|
+
path: "/api/admin/users/[id]/notifications",
|
|
98
|
+
handlerExport: "POST",
|
|
99
|
+
entryPoint: "api/admin/users/[id]/notifications",
|
|
100
|
+
authRequired: true,
|
|
101
|
+
},
|
|
83
102
|
],
|
|
84
103
|
migrations: {
|
|
85
104
|
enabled: true,
|
|
@@ -89,6 +108,7 @@ const authBuildConfig = {
|
|
|
89
108
|
"20251112000000_user_init.sql",
|
|
90
109
|
"20251112000001_auto_profile_and_admin_view.sql",
|
|
91
110
|
"20251112000002_sync_avatars.sql",
|
|
111
|
+
"20251124000001_add_get_admin_user_details.sql",
|
|
92
112
|
],
|
|
93
113
|
migrationsDownPath: "supabase/migrations-down",
|
|
94
114
|
},
|
|
@@ -149,5 +169,31 @@ const authBuildConfig = {
|
|
|
149
169
|
},
|
|
150
170
|
],
|
|
151
171
|
},
|
|
172
|
+
realtime: {
|
|
173
|
+
moduleId: "module-auth",
|
|
174
|
+
tables: [
|
|
175
|
+
{
|
|
176
|
+
schema: "public",
|
|
177
|
+
table: "user_notifications",
|
|
178
|
+
event: "*",
|
|
179
|
+
filter: "owner_id=eq.${USER_ID}",
|
|
180
|
+
broadcast: "user_notifications_updated",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
schema: "public",
|
|
184
|
+
table: "user_profil",
|
|
185
|
+
event: "*",
|
|
186
|
+
filter: "owner_id=eq.${USER_ID}",
|
|
187
|
+
broadcast: "user_profil_updated",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
schema: "public",
|
|
191
|
+
table: "user_addresses",
|
|
192
|
+
event: "*",
|
|
193
|
+
filter: "owner_id=eq.${USER_ID}",
|
|
194
|
+
broadcast: "user_address_updated",
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
152
198
|
};
|
|
153
199
|
export default authBuildConfig;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { DashboardPage } from "./web/auth/dashboard.js";
|
|
|
5
5
|
export { ProfilePage } from "./web/auth/profile.js";
|
|
6
6
|
export { ReglagePage } from "./web/auth/reglage.js";
|
|
7
7
|
export { AdminUsersPage } from "./web/admin/users.js";
|
|
8
|
+
export { default as UserPage } from "./web/admin/users/[id].js";
|
|
8
9
|
export { AuthModuleDoc } from "./components/Doc.js";
|
|
9
10
|
export { default as authBuildConfig } from "./auth.build.config.js";
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAEhE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { DashboardPage } from "./web/auth/dashboard.js";
|
|
|
6
6
|
export { ProfilePage } from "./web/auth/profile.js";
|
|
7
7
|
export { ReglagePage } from "./web/auth/reglage.js";
|
|
8
8
|
export { AdminUsersPage } from "./web/admin/users.js";
|
|
9
|
+
export { default as UserPage } from "./web/admin/users/[id].js";
|
|
9
10
|
// Documentation
|
|
10
11
|
export { AuthModuleDoc } from "./components/Doc.js";
|
|
11
12
|
// Configuration de build (utilisée par les scripts)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-detail.d.ts","sourceRoot":"","sources":["../../../src/web/admin/user-detail.tsx"],"names":[],"mappings":"AAkCA,UAAU,mBAAmB;IAC3B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,mBAAmB,2CAqT7D"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { Card, CardHeader, CardBody, Tabs, Tab, Avatar, Chip, Button, Input, Textarea, Select, SelectItem, Spinner, addToast, } from "@lastbrain/ui";
|
|
5
|
+
import { User, Bell, Settings } from "lucide-react";
|
|
6
|
+
import { useAuth } from "@lastbrain/core";
|
|
7
|
+
export function UserDetailPage({ userId }) {
|
|
8
|
+
const { user: _currentUser } = useAuth();
|
|
9
|
+
const [userProfile, setUserProfile] = useState(null);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [notificationTitle, setNotificationTitle] = useState("");
|
|
12
|
+
const [notificationMessage, setNotificationMessage] = useState("");
|
|
13
|
+
const [notificationType, setNotificationType] = useState("info");
|
|
14
|
+
const [sendingNotification, setSendingNotification] = useState(false);
|
|
15
|
+
// Fonction pour charger les données de l'utilisateur
|
|
16
|
+
const fetchUserProfile = useCallback(async () => {
|
|
17
|
+
try {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
// Utiliser l'API route pour récupérer les détails de l'utilisateur
|
|
20
|
+
const response = await fetch(`/api/admin/users/${userId}`);
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
if (response.status === 404) {
|
|
23
|
+
console.error("Utilisateur non trouvé");
|
|
24
|
+
setUserProfile(null);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const errorData = await response.json();
|
|
28
|
+
throw new Error(errorData.error || "Failed to fetch user details");
|
|
29
|
+
}
|
|
30
|
+
const userDetails = await response.json();
|
|
31
|
+
setUserProfile(userDetails);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error("Erreur lors du chargement du profil:", error);
|
|
35
|
+
setUserProfile(null);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
}, [userId]);
|
|
41
|
+
// Charger les données de l'utilisateur
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
fetchUserProfile();
|
|
44
|
+
}, [fetchUserProfile]);
|
|
45
|
+
const handleSendNotification = async () => {
|
|
46
|
+
if (!notificationTitle.trim() || !notificationMessage.trim()) {
|
|
47
|
+
addToast({
|
|
48
|
+
color: "danger",
|
|
49
|
+
title: "Veuillez remplir le titre et le message",
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
setSendingNotification(true);
|
|
55
|
+
// Utiliser l'API route pour envoyer la notification
|
|
56
|
+
const response = await fetch(`/api/admin/users/${userId}/notifications`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
title: notificationTitle.trim(),
|
|
63
|
+
message: notificationMessage.trim(),
|
|
64
|
+
type: notificationType,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const errorData = await response.json();
|
|
69
|
+
throw new Error(errorData.error || "Failed to send notification");
|
|
70
|
+
}
|
|
71
|
+
// Reset du formulaire
|
|
72
|
+
setNotificationTitle("");
|
|
73
|
+
setNotificationMessage("");
|
|
74
|
+
setNotificationType("info");
|
|
75
|
+
addToast({
|
|
76
|
+
color: "success",
|
|
77
|
+
title: "Notification envoyée avec succès",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
addToast({
|
|
82
|
+
color: "danger",
|
|
83
|
+
title: "Erreur lors de l'envoi de la notification",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
setSendingNotification(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
if (loading) {
|
|
91
|
+
return (_jsx("div", { className: "flex justify-center items-center min-h-64", children: _jsx(Spinner, { size: "lg" }) }));
|
|
92
|
+
}
|
|
93
|
+
if (!userProfile) {
|
|
94
|
+
return (_jsx(Card, { children: _jsx(CardBody, { children: _jsx("p", { className: "text-center text-gray-500", children: "Utilisateur non trouv\u00E9" }) }) }));
|
|
95
|
+
}
|
|
96
|
+
const isAdmin = Array.isArray(userProfile.raw_app_meta_data?.roles) &&
|
|
97
|
+
userProfile.raw_app_meta_data.roles.includes("admin");
|
|
98
|
+
return (_jsxs("div", { className: "mt-4 space-y-6", children: [_jsx(Card, { children: _jsxs(CardHeader, { className: "flex gap-4", children: [_jsx(Avatar, { src: userProfile.avatar_url, name: userProfile.full_name || userProfile.email, size: "lg" }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h1", { className: "text-2xl font-bold", children: userProfile.full_name || userProfile.email }), _jsx("p", { className: "text-gray-500", children: userProfile.email }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Chip, { color: isAdmin ? "danger" : "primary", size: "sm", children: isAdmin ? "Administrateur" : "Utilisateur" }), _jsxs(Chip, { color: "default", size: "sm", children: ["ID: ", userId] })] })] })] }) }), _jsx(Card, { children: _jsx(CardBody, { children: _jsxs(Tabs, { "aria-label": "Options utilisateur", color: "primary", variant: "underlined", children: [_jsx(Tab, { title: _jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(User, { size: 16 }), _jsx("span", { children: "Profil" })] }), children: _jsx("div", { className: "space-y-4 mt-4", children: _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "font-semibold mb-2", children: "Informations personnelles" }), _jsxs("div", { className: "space-y-2 text-sm", children: [_jsxs("p", { children: [_jsx("span", { className: "font-medium", children: "Nom complet:" }), " ", userProfile.full_name || "Non renseigné"] }), _jsxs("p", { children: [_jsx("span", { className: "font-medium", children: "Email:" }), " ", userProfile.email] }), _jsxs("p", { children: [_jsx("span", { className: "font-medium", children: "Date de cr\u00E9ation:" }), " ", userProfile.created_at
|
|
99
|
+
? new Date(userProfile.created_at).toLocaleDateString()
|
|
100
|
+
: "N/A"] }), _jsxs("p", { children: [_jsx("span", { className: "font-medium", children: "Derni\u00E8re connexion:" }), " ", userProfile.last_sign_in_at
|
|
101
|
+
? new Date(userProfile.last_sign_in_at).toLocaleDateString()
|
|
102
|
+
: "N/A"] })] })] }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold mb-2", children: "R\u00F4les et permissions" }), _jsx("div", { className: "space-y-2", children: _jsx(Chip, { color: isAdmin ? "danger" : "default", children: isAdmin ? "Administrateur" : "Utilisateur standard" }) })] })] }) }) }, "profile"), _jsx(Tab, { title: _jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(Bell, { size: 16 }), _jsx("span", { children: "Notifications" })] }), children: _jsxs("div", { className: "space-y-4 mt-4", children: [_jsx("h3", { className: "font-semibold", children: "Envoyer une notification" }), _jsxs("div", { className: "space-y-4", children: [_jsx(Input, { label: "Titre de la notification", placeholder: "Ex: Nouveau message important", value: notificationTitle, onChange: (e) => setNotificationTitle(e.target.value), maxLength: 100 }), _jsx(Textarea, { label: "Message", placeholder: "Contenu de la notification...", value: notificationMessage, onChange: (e) => setNotificationMessage(e.target.value), maxLength: 500, minRows: 3 }), _jsxs(Select, { label: "Type de notification", selectedKeys: [notificationType], onSelectionChange: (keys) => setNotificationType(Array.from(keys)[0]), children: [_jsx(SelectItem, { children: "Information" }, "info"), _jsx(SelectItem, { children: "Avertissement" }, "warning"), _jsx(SelectItem, { children: "Danger" }, "danger"), _jsx(SelectItem, { children: "Succ\u00E8s" }, "success")] }), _jsx(Button, { color: "primary", onPress: handleSendNotification, isLoading: sendingNotification, startContent: _jsx(Bell, { size: 16 }), isDisabled: !notificationTitle.trim() || !notificationMessage.trim(), children: "Envoyer la notification" })] })] }) }, "notifications"), _jsx(Tab, { title: _jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(Settings, { size: 16 }), _jsx("span", { children: "Param\u00E8tres" })] }), children: _jsxs("div", { className: "space-y-4 mt-4", children: [_jsx("h3", { className: "font-semibold", children: "Actions administrateur" }), _jsxs("div", { className: "space-y-3 space-x-5", children: [_jsx(Button, { color: "warning", variant: "bordered", size: "sm", children: "R\u00E9initialiser le mot de passe" }), _jsx(Button, { color: "danger", variant: "bordered", size: "sm", children: "Suspendre le compte" }), _jsx(Button, { color: "secondary", variant: "bordered", size: "sm", children: "Promouvoir en administrateur" })] }), _jsxs("div", { className: "mt-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg", children: [_jsx("h4", { className: "font-medium mb-2", children: "M\u00E9tadonn\u00E9es techniques" }), _jsx("pre", { className: "text-xs text-gray-600 dark:text-gray-400 overflow-auto", children: JSON.stringify({
|
|
103
|
+
app_metadata: userProfile.raw_app_meta_data,
|
|
104
|
+
user_metadata: userProfile.raw_user_meta_data,
|
|
105
|
+
}, null, 2) })] })] }) }, "settings")] }) }) })] }));
|
|
106
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"[id].d.ts","sourceRoot":"","sources":["../../../../src/web/admin/users/[id].tsx"],"names":[],"mappings":"AAEA,UAAU,aAAa;IACrB,MAAM,EAAE,OAAO,CAAC;QACd,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC,CAAC;CACJ;AAED,wBAA8B,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,aAAa,oDAG/D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"AAuCA,wBAAgB,cAAc,4CAuO7B"}
|
package/dist/web/admin/users.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useState } from "react";
|
|
4
|
-
import { Card, CardBody, CardHeader, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Spinner,
|
|
5
|
-
import { Users, Search, RefreshCw } from "lucide-react";
|
|
3
|
+
import { useCallback, useEffect, useState, useId } from "react";
|
|
4
|
+
import { Card, CardBody, CardHeader, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Spinner, Input, Button, Pagination, Avatar, } from "@lastbrain/ui";
|
|
5
|
+
import { Users, Search, RefreshCw, Eye } from "lucide-react";
|
|
6
|
+
import { useRouter } from "next/navigation";
|
|
6
7
|
export function AdminUsersPage() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const searchInputId = useId();
|
|
7
10
|
const [users, setUsers] = useState([]);
|
|
8
11
|
const [isLoading, setIsLoading] = useState(true);
|
|
9
12
|
const [error, setError] = useState(null);
|
|
@@ -17,40 +20,35 @@ export function AdminUsersPage() {
|
|
|
17
20
|
const fetchUsers = useCallback(async () => {
|
|
18
21
|
try {
|
|
19
22
|
setIsLoading(true);
|
|
23
|
+
// Utiliser l'API route au lieu d'appeler directement Supabase
|
|
20
24
|
const params = new URLSearchParams({
|
|
21
25
|
page: pagination.page.toString(),
|
|
22
26
|
per_page: pagination.per_page.toString(),
|
|
23
27
|
});
|
|
24
|
-
if (searchQuery) {
|
|
25
|
-
params.append("search", searchQuery);
|
|
28
|
+
if (searchQuery.trim()) {
|
|
29
|
+
params.append("search", searchQuery.trim());
|
|
26
30
|
}
|
|
27
31
|
const response = await fetch(`/api/admin/users?${params}`);
|
|
28
|
-
if (response.status === 403) {
|
|
29
|
-
setError("You don't have permission to access this page");
|
|
30
|
-
addToast({
|
|
31
|
-
title: "Access Denied",
|
|
32
|
-
description: "Superadmin access required",
|
|
33
|
-
color: "danger",
|
|
34
|
-
});
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
32
|
if (!response.ok) {
|
|
38
|
-
|
|
33
|
+
const errorData = await response.json();
|
|
34
|
+
throw new Error(errorData.error || "Failed to fetch users");
|
|
39
35
|
}
|
|
40
36
|
const result = await response.json();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
// L'API retourne soit { data, pagination } soit directement { users, pagination }
|
|
38
|
+
const usersData = result.data || result.users || [];
|
|
39
|
+
const paginationData = result.pagination || {
|
|
40
|
+
page: pagination.page,
|
|
41
|
+
per_page: pagination.per_page,
|
|
42
|
+
total: 0,
|
|
43
|
+
total_pages: 0,
|
|
44
|
+
};
|
|
45
|
+
setUsers(usersData);
|
|
46
|
+
setPagination(paginationData);
|
|
45
47
|
setError(null);
|
|
46
48
|
}
|
|
47
49
|
catch (err) {
|
|
48
50
|
setError(err instanceof Error ? err.message : "An error occurred");
|
|
49
|
-
|
|
50
|
-
title: "Error",
|
|
51
|
-
description: "Failed to load users",
|
|
52
|
-
color: "danger",
|
|
53
|
-
});
|
|
51
|
+
console.error("Erreur lors du chargement des utilisateurs:", err);
|
|
54
52
|
}
|
|
55
53
|
finally {
|
|
56
54
|
setIsLoading(false);
|
|
@@ -59,15 +57,17 @@ export function AdminUsersPage() {
|
|
|
59
57
|
useEffect(() => {
|
|
60
58
|
fetchUsers();
|
|
61
59
|
}, [fetchUsers]);
|
|
62
|
-
const handleSearch = () => {
|
|
60
|
+
const handleSearch = useCallback(() => {
|
|
63
61
|
setPagination((prev) => ({ ...prev, page: 1 }));
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const handlePageChange = (page) => {
|
|
62
|
+
}, []);
|
|
63
|
+
const handlePageChange = useCallback((page) => {
|
|
67
64
|
setPagination((prev) => ({ ...prev, page }));
|
|
68
|
-
};
|
|
65
|
+
}, []);
|
|
66
|
+
const handleViewUser = useCallback((userId) => {
|
|
67
|
+
router.push(`/admin/auth/users/${userId}`);
|
|
68
|
+
}, [router]);
|
|
69
69
|
const formatDate = (dateString) => {
|
|
70
|
-
return new Date(dateString).toLocaleDateString("
|
|
70
|
+
return new Date(dateString).toLocaleDateString("fr-FR", {
|
|
71
71
|
year: "numeric",
|
|
72
72
|
month: "short",
|
|
73
73
|
day: "numeric",
|
|
@@ -76,21 +76,16 @@ export function AdminUsersPage() {
|
|
|
76
76
|
if (error && users.length === 0) {
|
|
77
77
|
return (_jsx("div", { className: "pt-12 pb-12 max-w-7xl mx-auto px-4", children: _jsx(Card, { children: _jsx(CardBody, { children: _jsx("p", { className: "text-danger", children: error }) }) }) }));
|
|
78
78
|
}
|
|
79
|
-
return (_jsxs("div", { className: "pt-12 pb-12 max-w-7xl mx-auto px-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-8", children: [_jsx(Users, { className: "w-8 h-8" }), _jsx("h1", { className: "text-3xl font-bold", children: "User Management" })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex flex-col md:flex-row gap-4 w-full", children: [_jsxs("div", { className: "flex gap-2 flex-1", children: [_jsx(Input, { placeholder: "Search by email or name...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onKeyPress: (e) => {
|
|
79
|
+
return (_jsxs("div", { className: "pt-12 pb-12 max-w-7xl mx-auto px-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-8", children: [_jsx(Users, { className: "w-8 h-8" }), _jsx("h1", { className: "text-3xl font-bold", children: "User Management" })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex flex-col md:flex-row gap-4 w-full", children: [_jsxs("div", { className: "flex gap-2 flex-1", children: [_jsx(Input, { id: searchInputId, placeholder: "Search by email or name...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onKeyPress: (e) => {
|
|
80
80
|
if (e.key === "Enter") {
|
|
81
81
|
handleSearch();
|
|
82
82
|
}
|
|
83
|
-
}, startContent: _jsx(Search, { className: "w-4 h-4 text-default-400" }), className: "flex-1" }), _jsx(Button, { color: "primary", onPress: handleSearch, isDisabled: isLoading, children: "Search" })] }), _jsx(Button, { variant: "flat", onPress: fetchUsers, isDisabled: isLoading, startContent: _jsx(RefreshCw, { className: "w-4 h-4" }), children: "Refresh" })] }) }), _jsx(CardBody, { children: isLoading ? (_jsx("div", { className: "flex justify-center items-center py-12", children: _jsx(Spinner, { size: "lg", label: "Loading users..." }) })) : users.length === 0 ? (_jsx("div", { className: "text-center py-12 text-default-500", children: "No users found" })) : (_jsxs(_Fragment, { children: [_jsxs(Table, { "aria-label": "Users table", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: "USER" }), _jsx(TableColumn, { children: "EMAIL" }), _jsx(TableColumn, { children: "
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
? "danger"
|
|
89
|
-
: user.role === "moderator"
|
|
90
|
-
? "secondary"
|
|
91
|
-
: "default";
|
|
92
|
-
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Avatar, { src: `/api/storage/${user.profile?.avatar_url}`, name: fullName, size: "sm" }), _jsx("span", { className: "text-small font-medium", children: fullName })] }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.email }) }), _jsx(TableCell, { children: _jsx(Chip, { color: roleColor, size: "sm", variant: "flat", children: user.role || "user" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.profile?.company || "-" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.last_sign_in_at
|
|
83
|
+
}, startContent: _jsx(Search, { className: "w-4 h-4 text-default-400" }), className: "flex-1" }), _jsx(Button, { color: "primary", onPress: handleSearch, isDisabled: isLoading, children: "Search" })] }), _jsx(Button, { variant: "flat", onPress: fetchUsers, isDisabled: isLoading, startContent: _jsx(RefreshCw, { className: "w-4 h-4" }), children: "Refresh" })] }) }), _jsx(CardBody, { children: isLoading ? (_jsx("div", { className: "flex justify-center items-center py-12", children: _jsx(Spinner, { size: "lg", label: "Loading users..." }) })) : users.length === 0 ? (_jsx("div", { className: "text-center py-12 text-default-500", children: "No users found" })) : (_jsxs(_Fragment, { children: [_jsxs(Table, { "aria-label": "Users table", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: "USER" }), _jsx(TableColumn, { children: "EMAIL" }), _jsx(TableColumn, { children: "LAST SIGN IN" }), _jsx(TableColumn, { children: "CREATED" }), _jsx(TableColumn, { children: "ACTIONS" })] }), _jsx(TableBody, { children: users.map((user) => {
|
|
84
|
+
const displayName = user.full_name || user.email;
|
|
85
|
+
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Avatar, { src: user.avatar_url
|
|
86
|
+
? `/api/storage/${user.avatar_url}`
|
|
87
|
+
: undefined, name: displayName, size: "sm" }), _jsx("span", { className: "text-small font-medium", children: displayName })] }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.email }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.last_sign_in_at
|
|
93
88
|
? formatDate(user.last_sign_in_at)
|
|
94
|
-
: "
|
|
89
|
+
: "Jamais" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: formatDate(user.created_at) }) }), _jsx(TableCell, { children: _jsx(Button, { size: "sm", variant: "flat", color: "primary", onPress: () => handleViewUser(user.id), startContent: _jsx(Eye, { size: 14 }), children: "Voir" }) })] }, user.id));
|
|
95
90
|
}) })] }), pagination.total_pages > 1 && (_jsx("div", { className: "flex justify-center mt-4", children: _jsx(Pagination, { total: pagination.total_pages, page: pagination.page, onChange: handlePageChange, showControls: true }) })), _jsxs("div", { className: "mt-4 text-small text-default-500 text-center", children: ["Showing ", users.length, " of ", pagination.total, " users"] })] })) })] })] }));
|
|
96
91
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* POST /api/admin/users/[id]/notifications
|
|
6
|
+
* Send a notification to a specific user (superadmin only)
|
|
7
|
+
*/
|
|
8
|
+
export async function POST(
|
|
9
|
+
request: NextRequest,
|
|
10
|
+
context: { params: Promise<{ id: string }> },
|
|
11
|
+
) {
|
|
12
|
+
try {
|
|
13
|
+
const supabase = await getSupabaseServerClient();
|
|
14
|
+
const { id: userId } = await context.params;
|
|
15
|
+
|
|
16
|
+
if (!userId) {
|
|
17
|
+
return NextResponse.json(
|
|
18
|
+
{ error: "User ID is required" },
|
|
19
|
+
{ status: 400 },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const body = await request.json();
|
|
24
|
+
const { title, message, type = "info" } = body;
|
|
25
|
+
|
|
26
|
+
if (!title?.trim() || !message?.trim()) {
|
|
27
|
+
return NextResponse.json(
|
|
28
|
+
{ error: "Title and message are required" },
|
|
29
|
+
{ status: 400 },
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Insérer la notification dans la base de données
|
|
34
|
+
const { data, error } = await supabase
|
|
35
|
+
.from("user_notifications")
|
|
36
|
+
.insert({
|
|
37
|
+
owner_id: userId,
|
|
38
|
+
title: title.trim(),
|
|
39
|
+
body: message.trim(),
|
|
40
|
+
type: type,
|
|
41
|
+
read: false,
|
|
42
|
+
})
|
|
43
|
+
.select()
|
|
44
|
+
.single();
|
|
45
|
+
|
|
46
|
+
if (error) {
|
|
47
|
+
console.error("Error creating notification:", error);
|
|
48
|
+
return NextResponse.json(
|
|
49
|
+
{ error: "Database Error", message: error.message },
|
|
50
|
+
{ status: 500 },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return NextResponse.json({
|
|
55
|
+
success: true,
|
|
56
|
+
notification: data,
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Error in send notification endpoint:", error);
|
|
60
|
+
return NextResponse.json(
|
|
61
|
+
{
|
|
62
|
+
error: "Internal Server Error",
|
|
63
|
+
message: "Failed to send notification",
|
|
64
|
+
},
|
|
65
|
+
{ status: 500 },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/admin/users/[id]
|
|
6
|
+
* Returns user details by ID (superadmin only)
|
|
7
|
+
*/
|
|
8
|
+
export async function GET(
|
|
9
|
+
request: NextRequest,
|
|
10
|
+
context: { params: Promise<{ id: string }> },
|
|
11
|
+
) {
|
|
12
|
+
try {
|
|
13
|
+
const supabase = await getSupabaseServerClient();
|
|
14
|
+
const { id: userId } = await context.params;
|
|
15
|
+
|
|
16
|
+
if (!userId) {
|
|
17
|
+
return NextResponse.json(
|
|
18
|
+
{ error: "User ID is required" },
|
|
19
|
+
{ status: 400 },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Utiliser la fonction RPC pour récupérer les détails de l'utilisateur
|
|
24
|
+
const { data: userDetails, error: userError } = await supabase.rpc(
|
|
25
|
+
"get_admin_user_details",
|
|
26
|
+
{ user_id: userId },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (userError) {
|
|
30
|
+
console.error("Error fetching user details:", userError);
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ error: "Database Error", message: userError.message },
|
|
33
|
+
{ status: 500 },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!userDetails) {
|
|
38
|
+
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return NextResponse.json(userDetails);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error("Error in admin user details endpoint:", error);
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{
|
|
46
|
+
error: "Internal Server Error",
|
|
47
|
+
message: "Failed to fetch user details",
|
|
48
|
+
},
|
|
49
|
+
{ status: 500 },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|