@lastbrain/module-auth 0.1.6 → 0.1.8
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
package/src/auth.build.config.ts
CHANGED
|
@@ -38,6 +38,11 @@ const authBuildConfig: ModuleBuildConfig = {
|
|
|
38
38
|
path: "/users",
|
|
39
39
|
componentExport: "AdminUsersPage",
|
|
40
40
|
},
|
|
41
|
+
{
|
|
42
|
+
section: "admin",
|
|
43
|
+
path: "/users/[id]",
|
|
44
|
+
componentExport: "UserPage",
|
|
45
|
+
},
|
|
41
46
|
],
|
|
42
47
|
apis: [
|
|
43
48
|
{
|
|
@@ -82,6 +87,20 @@ const authBuildConfig: ModuleBuildConfig = {
|
|
|
82
87
|
entryPoint: "api/admin/users",
|
|
83
88
|
authRequired: true,
|
|
84
89
|
},
|
|
90
|
+
{
|
|
91
|
+
method: "GET",
|
|
92
|
+
path: "/api/admin/users/[id]",
|
|
93
|
+
handlerExport: "GET",
|
|
94
|
+
entryPoint: "api/admin/users/[id]",
|
|
95
|
+
authRequired: true,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
method: "POST",
|
|
99
|
+
path: "/api/admin/users/[id]/notifications",
|
|
100
|
+
handlerExport: "POST",
|
|
101
|
+
entryPoint: "api/admin/users/[id]/notifications",
|
|
102
|
+
authRequired: true,
|
|
103
|
+
},
|
|
85
104
|
],
|
|
86
105
|
migrations: {
|
|
87
106
|
enabled: true,
|
|
@@ -91,6 +110,7 @@ const authBuildConfig: ModuleBuildConfig = {
|
|
|
91
110
|
"20251112000000_user_init.sql",
|
|
92
111
|
"20251112000001_auto_profile_and_admin_view.sql",
|
|
93
112
|
"20251112000002_sync_avatars.sql",
|
|
113
|
+
"20251124000001_add_get_admin_user_details.sql",
|
|
94
114
|
],
|
|
95
115
|
migrationsDownPath: "supabase/migrations-down",
|
|
96
116
|
},
|
|
@@ -151,6 +171,32 @@ const authBuildConfig: ModuleBuildConfig = {
|
|
|
151
171
|
},
|
|
152
172
|
],
|
|
153
173
|
},
|
|
174
|
+
realtime: {
|
|
175
|
+
moduleId: "module-auth",
|
|
176
|
+
tables: [
|
|
177
|
+
{
|
|
178
|
+
schema: "public",
|
|
179
|
+
table: "user_notifications",
|
|
180
|
+
event: "*",
|
|
181
|
+
filter: "owner_id=eq.${USER_ID}",
|
|
182
|
+
broadcast: "user_notifications_updated",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
schema: "public",
|
|
186
|
+
table: "user_profil",
|
|
187
|
+
event: "*",
|
|
188
|
+
filter: "owner_id=eq.${USER_ID}",
|
|
189
|
+
broadcast: "user_profil_updated",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
schema: "public",
|
|
193
|
+
table: "user_addresses",
|
|
194
|
+
event: "*",
|
|
195
|
+
filter: "owner_id=eq.${USER_ID}",
|
|
196
|
+
broadcast: "user_address_updated",
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
154
200
|
};
|
|
155
201
|
|
|
156
202
|
export default authBuildConfig;
|
package/src/index.ts
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,348 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardBody,
|
|
8
|
+
Tabs,
|
|
9
|
+
Tab,
|
|
10
|
+
Avatar,
|
|
11
|
+
Chip,
|
|
12
|
+
Button,
|
|
13
|
+
Input,
|
|
14
|
+
Textarea,
|
|
15
|
+
Select,
|
|
16
|
+
SelectItem,
|
|
17
|
+
Spacer,
|
|
18
|
+
Spinner,
|
|
19
|
+
addToast,
|
|
20
|
+
} from "@lastbrain/ui";
|
|
21
|
+
import { User, Bell, Settings, Save } from "lucide-react";
|
|
22
|
+
import { useAuth } from "@lastbrain/core";
|
|
23
|
+
|
|
24
|
+
interface UserProfile {
|
|
25
|
+
id: string;
|
|
26
|
+
email: string;
|
|
27
|
+
full_name: string;
|
|
28
|
+
avatar_url: string;
|
|
29
|
+
created_at: string;
|
|
30
|
+
last_sign_in_at: string;
|
|
31
|
+
raw_app_meta_data: Record<string, unknown>;
|
|
32
|
+
raw_user_meta_data: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface UserDetailPageProps {
|
|
36
|
+
userId: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function UserDetailPage({ userId }: UserDetailPageProps) {
|
|
40
|
+
const { user: _currentUser } = useAuth();
|
|
41
|
+
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
|
42
|
+
const [loading, setLoading] = useState(true);
|
|
43
|
+
const [notificationTitle, setNotificationTitle] = useState("");
|
|
44
|
+
const [notificationMessage, setNotificationMessage] = useState("");
|
|
45
|
+
const [notificationType, setNotificationType] = useState("info");
|
|
46
|
+
const [sendingNotification, setSendingNotification] = useState(false);
|
|
47
|
+
|
|
48
|
+
// Fonction pour charger les données de l'utilisateur
|
|
49
|
+
const fetchUserProfile = useCallback(async () => {
|
|
50
|
+
try {
|
|
51
|
+
setLoading(true);
|
|
52
|
+
|
|
53
|
+
// Utiliser l'API route pour récupérer les détails de l'utilisateur
|
|
54
|
+
const response = await fetch(`/api/admin/users/${userId}`);
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
if (response.status === 404) {
|
|
58
|
+
console.error("Utilisateur non trouvé");
|
|
59
|
+
setUserProfile(null);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const errorData = await response.json();
|
|
63
|
+
throw new Error(errorData.error || "Failed to fetch user details");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const userDetails = await response.json();
|
|
67
|
+
setUserProfile(userDetails);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("Erreur lors du chargement du profil:", error);
|
|
70
|
+
setUserProfile(null);
|
|
71
|
+
} finally {
|
|
72
|
+
setLoading(false);
|
|
73
|
+
}
|
|
74
|
+
}, [userId]);
|
|
75
|
+
|
|
76
|
+
// Charger les données de l'utilisateur
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
fetchUserProfile();
|
|
79
|
+
}, [fetchUserProfile]);
|
|
80
|
+
|
|
81
|
+
const handleSendNotification = async () => {
|
|
82
|
+
if (!notificationTitle.trim() || !notificationMessage.trim()) {
|
|
83
|
+
addToast({
|
|
84
|
+
color: "danger",
|
|
85
|
+
title: "Veuillez remplir le titre et le message",
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
setSendingNotification(true);
|
|
92
|
+
|
|
93
|
+
// Utiliser l'API route pour envoyer la notification
|
|
94
|
+
const response = await fetch(`/api/admin/users/${userId}/notifications`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
title: notificationTitle.trim(),
|
|
101
|
+
message: notificationMessage.trim(),
|
|
102
|
+
type: notificationType,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const errorData = await response.json();
|
|
108
|
+
throw new Error(errorData.error || "Failed to send notification");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Reset du formulaire
|
|
112
|
+
setNotificationTitle("");
|
|
113
|
+
setNotificationMessage("");
|
|
114
|
+
setNotificationType("info");
|
|
115
|
+
|
|
116
|
+
addToast({
|
|
117
|
+
color: "success",
|
|
118
|
+
title: "Notification envoyée avec succès",
|
|
119
|
+
});
|
|
120
|
+
} catch {
|
|
121
|
+
addToast({
|
|
122
|
+
color: "danger",
|
|
123
|
+
title: "Erreur lors de l'envoi de la notification",
|
|
124
|
+
});
|
|
125
|
+
} finally {
|
|
126
|
+
setSendingNotification(false);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (loading) {
|
|
131
|
+
return (
|
|
132
|
+
<div className="flex justify-center items-center min-h-64">
|
|
133
|
+
<Spinner size="lg" />
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!userProfile) {
|
|
139
|
+
return (
|
|
140
|
+
<Card>
|
|
141
|
+
<CardBody>
|
|
142
|
+
<p className="text-center text-gray-500">Utilisateur non trouvé</p>
|
|
143
|
+
</CardBody>
|
|
144
|
+
</Card>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const isAdmin =
|
|
149
|
+
Array.isArray(userProfile.raw_app_meta_data?.roles) &&
|
|
150
|
+
userProfile.raw_app_meta_data.roles.includes("admin");
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="mt-4 space-y-6">
|
|
154
|
+
{/* Header utilisateur */}
|
|
155
|
+
<Card>
|
|
156
|
+
<CardHeader className="flex gap-4">
|
|
157
|
+
<Avatar
|
|
158
|
+
src={userProfile.avatar_url}
|
|
159
|
+
name={userProfile.full_name || userProfile.email}
|
|
160
|
+
size="lg"
|
|
161
|
+
/>
|
|
162
|
+
<div className="flex flex-col gap-1">
|
|
163
|
+
<h1 className="text-2xl font-bold">
|
|
164
|
+
{userProfile.full_name || userProfile.email}
|
|
165
|
+
</h1>
|
|
166
|
+
<p className="text-gray-500">{userProfile.email}</p>
|
|
167
|
+
<div className="flex gap-2">
|
|
168
|
+
<Chip color={isAdmin ? "danger" : "primary"} size="sm">
|
|
169
|
+
{isAdmin ? "Administrateur" : "Utilisateur"}
|
|
170
|
+
</Chip>
|
|
171
|
+
<Chip color="default" size="sm">
|
|
172
|
+
ID: {userId}
|
|
173
|
+
</Chip>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</CardHeader>
|
|
177
|
+
</Card>
|
|
178
|
+
|
|
179
|
+
{/* Tabs */}
|
|
180
|
+
<Card>
|
|
181
|
+
<CardBody>
|
|
182
|
+
<Tabs
|
|
183
|
+
aria-label="Options utilisateur"
|
|
184
|
+
color="primary"
|
|
185
|
+
variant="underlined"
|
|
186
|
+
>
|
|
187
|
+
{/* Tab Profil */}
|
|
188
|
+
<Tab
|
|
189
|
+
key="profile"
|
|
190
|
+
title={
|
|
191
|
+
<div className="flex items-center space-x-2">
|
|
192
|
+
<User size={16} />
|
|
193
|
+
<span>Profil</span>
|
|
194
|
+
</div>
|
|
195
|
+
}
|
|
196
|
+
>
|
|
197
|
+
<div className="space-y-4 mt-4">
|
|
198
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
199
|
+
<div>
|
|
200
|
+
<h3 className="font-semibold mb-2">
|
|
201
|
+
Informations personnelles
|
|
202
|
+
</h3>
|
|
203
|
+
<div className="space-y-2 text-sm">
|
|
204
|
+
<p>
|
|
205
|
+
<span className="font-medium">Nom complet:</span>{" "}
|
|
206
|
+
{userProfile.full_name || "Non renseigné"}
|
|
207
|
+
</p>
|
|
208
|
+
<p>
|
|
209
|
+
<span className="font-medium">Email:</span>{" "}
|
|
210
|
+
{userProfile.email}
|
|
211
|
+
</p>
|
|
212
|
+
<p>
|
|
213
|
+
<span className="font-medium">Date de création:</span>{" "}
|
|
214
|
+
{userProfile.created_at
|
|
215
|
+
? new Date(
|
|
216
|
+
userProfile.created_at,
|
|
217
|
+
).toLocaleDateString()
|
|
218
|
+
: "N/A"}
|
|
219
|
+
</p>
|
|
220
|
+
<p>
|
|
221
|
+
<span className="font-medium">Dernière connexion:</span>{" "}
|
|
222
|
+
{userProfile.last_sign_in_at
|
|
223
|
+
? new Date(
|
|
224
|
+
userProfile.last_sign_in_at,
|
|
225
|
+
).toLocaleDateString()
|
|
226
|
+
: "N/A"}
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<div>
|
|
231
|
+
<h3 className="font-semibold mb-2">Rôles et permissions</h3>
|
|
232
|
+
<div className="space-y-2">
|
|
233
|
+
<Chip color={isAdmin ? "danger" : "default"}>
|
|
234
|
+
{isAdmin ? "Administrateur" : "Utilisateur standard"}
|
|
235
|
+
</Chip>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</Tab>
|
|
241
|
+
|
|
242
|
+
{/* Tab Notifications */}
|
|
243
|
+
<Tab
|
|
244
|
+
key="notifications"
|
|
245
|
+
title={
|
|
246
|
+
<div className="flex items-center space-x-2">
|
|
247
|
+
<Bell size={16} />
|
|
248
|
+
<span>Notifications</span>
|
|
249
|
+
</div>
|
|
250
|
+
}
|
|
251
|
+
>
|
|
252
|
+
<div className="space-y-4 mt-4">
|
|
253
|
+
<h3 className="font-semibold">Envoyer une notification</h3>
|
|
254
|
+
|
|
255
|
+
<div className="space-y-4">
|
|
256
|
+
<Input
|
|
257
|
+
label="Titre de la notification"
|
|
258
|
+
placeholder="Ex: Nouveau message important"
|
|
259
|
+
value={notificationTitle}
|
|
260
|
+
onChange={(e) => setNotificationTitle(e.target.value)}
|
|
261
|
+
maxLength={100}
|
|
262
|
+
/>
|
|
263
|
+
|
|
264
|
+
<Textarea
|
|
265
|
+
label="Message"
|
|
266
|
+
placeholder="Contenu de la notification..."
|
|
267
|
+
value={notificationMessage}
|
|
268
|
+
onChange={(e) => setNotificationMessage(e.target.value)}
|
|
269
|
+
maxLength={500}
|
|
270
|
+
minRows={3}
|
|
271
|
+
/>
|
|
272
|
+
|
|
273
|
+
<Select
|
|
274
|
+
label="Type de notification"
|
|
275
|
+
selectedKeys={[notificationType]}
|
|
276
|
+
onSelectionChange={(keys) =>
|
|
277
|
+
setNotificationType(Array.from(keys)[0] as string)
|
|
278
|
+
}
|
|
279
|
+
>
|
|
280
|
+
<SelectItem key="info">Information</SelectItem>
|
|
281
|
+
<SelectItem key="warning">Avertissement</SelectItem>
|
|
282
|
+
<SelectItem key="danger">Danger</SelectItem>
|
|
283
|
+
<SelectItem key="success">Succès</SelectItem>
|
|
284
|
+
</Select>
|
|
285
|
+
|
|
286
|
+
<Button
|
|
287
|
+
color="primary"
|
|
288
|
+
onPress={handleSendNotification}
|
|
289
|
+
isLoading={sendingNotification}
|
|
290
|
+
startContent={<Bell size={16} />}
|
|
291
|
+
isDisabled={
|
|
292
|
+
!notificationTitle.trim() || !notificationMessage.trim()
|
|
293
|
+
}
|
|
294
|
+
>
|
|
295
|
+
Envoyer la notification
|
|
296
|
+
</Button>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</Tab>
|
|
300
|
+
|
|
301
|
+
{/* Tab Paramètres */}
|
|
302
|
+
<Tab
|
|
303
|
+
key="settings"
|
|
304
|
+
title={
|
|
305
|
+
<div className="flex items-center space-x-2">
|
|
306
|
+
<Settings size={16} />
|
|
307
|
+
<span>Paramètres</span>
|
|
308
|
+
</div>
|
|
309
|
+
}
|
|
310
|
+
>
|
|
311
|
+
<div className="space-y-4 mt-4">
|
|
312
|
+
<h3 className="font-semibold">Actions administrateur</h3>
|
|
313
|
+
|
|
314
|
+
<div className="space-y-3 space-x-5">
|
|
315
|
+
<Button color="warning" variant="bordered" size="sm">
|
|
316
|
+
Réinitialiser le mot de passe
|
|
317
|
+
</Button>
|
|
318
|
+
|
|
319
|
+
<Button color="danger" variant="bordered" size="sm">
|
|
320
|
+
Suspendre le compte
|
|
321
|
+
</Button>
|
|
322
|
+
|
|
323
|
+
<Button color="secondary" variant="bordered" size="sm">
|
|
324
|
+
Promouvoir en administrateur
|
|
325
|
+
</Button>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
329
|
+
<h4 className="font-medium mb-2">Métadonnées techniques</h4>
|
|
330
|
+
<pre className="text-xs text-gray-600 dark:text-gray-400 overflow-auto">
|
|
331
|
+
{JSON.stringify(
|
|
332
|
+
{
|
|
333
|
+
app_metadata: userProfile.raw_app_meta_data,
|
|
334
|
+
user_metadata: userProfile.raw_user_meta_data,
|
|
335
|
+
},
|
|
336
|
+
null,
|
|
337
|
+
2,
|
|
338
|
+
)}
|
|
339
|
+
</pre>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</Tab>
|
|
343
|
+
</Tabs>
|
|
344
|
+
</CardBody>
|
|
345
|
+
</Card>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { UserDetailPage } from "../user-detail.js";
|
|
2
|
+
|
|
3
|
+
interface UserPageProps {
|
|
4
|
+
params: Promise<{
|
|
5
|
+
id: string;
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default async function UserPage({ params }: UserPageProps) {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
return <UserDetailPage userId={id} />;
|
|
12
|
+
}
|
package/src/web/admin/users.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useState, useId } from "react";
|
|
4
4
|
import {
|
|
5
5
|
Card,
|
|
6
6
|
CardBody,
|
|
@@ -17,32 +17,17 @@ import {
|
|
|
17
17
|
Button,
|
|
18
18
|
Pagination,
|
|
19
19
|
Avatar,
|
|
20
|
-
addToast,
|
|
21
20
|
} from "@lastbrain/ui";
|
|
22
|
-
import { Users, Search, RefreshCw } from "lucide-react";
|
|
21
|
+
import { Users, Search, RefreshCw, Eye } from "lucide-react";
|
|
22
|
+
import { useRouter } from "next/navigation";
|
|
23
23
|
|
|
24
24
|
interface User {
|
|
25
25
|
id: string;
|
|
26
26
|
email: string;
|
|
27
27
|
created_at: string;
|
|
28
|
-
email_confirmed_at?: string;
|
|
29
28
|
last_sign_in_at?: string;
|
|
30
|
-
role?: string;
|
|
31
29
|
full_name?: string;
|
|
32
|
-
|
|
33
|
-
metadata?: Record<string, unknown>;
|
|
34
|
-
profile: {
|
|
35
|
-
first_name?: string;
|
|
36
|
-
last_name?: string;
|
|
37
|
-
avatar_url?: string;
|
|
38
|
-
company?: string;
|
|
39
|
-
location?: string;
|
|
40
|
-
bio?: string;
|
|
41
|
-
phone?: string;
|
|
42
|
-
website?: string;
|
|
43
|
-
language?: string;
|
|
44
|
-
timezone?: string;
|
|
45
|
-
};
|
|
30
|
+
avatar_url?: string;
|
|
46
31
|
}
|
|
47
32
|
|
|
48
33
|
interface PaginationData {
|
|
@@ -53,6 +38,8 @@ interface PaginationData {
|
|
|
53
38
|
}
|
|
54
39
|
|
|
55
40
|
export function AdminUsersPage() {
|
|
41
|
+
const router = useRouter();
|
|
42
|
+
const searchInputId = useId();
|
|
56
43
|
const [users, setUsers] = useState<User[]>([]);
|
|
57
44
|
const [isLoading, setIsLoading] = useState(true);
|
|
58
45
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -67,44 +54,41 @@ export function AdminUsersPage() {
|
|
|
67
54
|
const fetchUsers = useCallback(async () => {
|
|
68
55
|
try {
|
|
69
56
|
setIsLoading(true);
|
|
57
|
+
|
|
58
|
+
// Utiliser l'API route au lieu d'appeler directement Supabase
|
|
70
59
|
const params = new URLSearchParams({
|
|
71
60
|
page: pagination.page.toString(),
|
|
72
61
|
per_page: pagination.per_page.toString(),
|
|
73
62
|
});
|
|
74
63
|
|
|
75
|
-
if (searchQuery) {
|
|
76
|
-
params.append("search", searchQuery);
|
|
64
|
+
if (searchQuery.trim()) {
|
|
65
|
+
params.append("search", searchQuery.trim());
|
|
77
66
|
}
|
|
78
67
|
|
|
79
68
|
const response = await fetch(`/api/admin/users?${params}`);
|
|
80
69
|
|
|
81
|
-
if (response.status === 403) {
|
|
82
|
-
setError("You don't have permission to access this page");
|
|
83
|
-
addToast({
|
|
84
|
-
title: "Access Denied",
|
|
85
|
-
description: "Superadmin access required",
|
|
86
|
-
color: "danger",
|
|
87
|
-
});
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
70
|
if (!response.ok) {
|
|
92
|
-
|
|
71
|
+
const errorData = await response.json();
|
|
72
|
+
throw new Error(errorData.error || "Failed to fetch users");
|
|
93
73
|
}
|
|
94
74
|
|
|
95
75
|
const result = await response.json();
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
76
|
+
|
|
77
|
+
// L'API retourne soit { data, pagination } soit directement { users, pagination }
|
|
78
|
+
const usersData = result.data || result.users || [];
|
|
79
|
+
const paginationData = result.pagination || {
|
|
80
|
+
page: pagination.page,
|
|
81
|
+
per_page: pagination.per_page,
|
|
82
|
+
total: 0,
|
|
83
|
+
total_pages: 0,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
setUsers(usersData);
|
|
87
|
+
setPagination(paginationData);
|
|
100
88
|
setError(null);
|
|
101
89
|
} catch (err) {
|
|
102
90
|
setError(err instanceof Error ? err.message : "An error occurred");
|
|
103
|
-
|
|
104
|
-
title: "Error",
|
|
105
|
-
description: "Failed to load users",
|
|
106
|
-
color: "danger",
|
|
107
|
-
});
|
|
91
|
+
console.error("Erreur lors du chargement des utilisateurs:", err);
|
|
108
92
|
} finally {
|
|
109
93
|
setIsLoading(false);
|
|
110
94
|
}
|
|
@@ -114,17 +98,23 @@ export function AdminUsersPage() {
|
|
|
114
98
|
fetchUsers();
|
|
115
99
|
}, [fetchUsers]);
|
|
116
100
|
|
|
117
|
-
const handleSearch = () => {
|
|
101
|
+
const handleSearch = useCallback(() => {
|
|
118
102
|
setPagination((prev) => ({ ...prev, page: 1 }));
|
|
119
|
-
|
|
120
|
-
};
|
|
103
|
+
}, []);
|
|
121
104
|
|
|
122
|
-
const handlePageChange = (page: number) => {
|
|
105
|
+
const handlePageChange = useCallback((page: number) => {
|
|
123
106
|
setPagination((prev) => ({ ...prev, page }));
|
|
124
|
-
};
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
const handleViewUser = useCallback(
|
|
110
|
+
(userId: string) => {
|
|
111
|
+
router.push(`/admin/auth/users/${userId}`);
|
|
112
|
+
},
|
|
113
|
+
[router],
|
|
114
|
+
);
|
|
125
115
|
|
|
126
116
|
const formatDate = (dateString: string) => {
|
|
127
|
-
return new Date(dateString).toLocaleDateString("
|
|
117
|
+
return new Date(dateString).toLocaleDateString("fr-FR", {
|
|
128
118
|
year: "numeric",
|
|
129
119
|
month: "short",
|
|
130
120
|
day: "numeric",
|
|
@@ -155,6 +145,7 @@ export function AdminUsersPage() {
|
|
|
155
145
|
<div className="flex flex-col md:flex-row gap-4 w-full">
|
|
156
146
|
<div className="flex gap-2 flex-1">
|
|
157
147
|
<Input
|
|
148
|
+
id={searchInputId}
|
|
158
149
|
placeholder="Search by email or name..."
|
|
159
150
|
value={searchQuery}
|
|
160
151
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
@@ -199,58 +190,40 @@ export function AdminUsersPage() {
|
|
|
199
190
|
<TableHeader>
|
|
200
191
|
<TableColumn>USER</TableColumn>
|
|
201
192
|
<TableColumn>EMAIL</TableColumn>
|
|
202
|
-
<TableColumn>ROLE</TableColumn>
|
|
203
|
-
<TableColumn>COMPANY</TableColumn>
|
|
204
193
|
<TableColumn>LAST SIGN IN</TableColumn>
|
|
205
194
|
<TableColumn>CREATED</TableColumn>
|
|
206
|
-
<TableColumn>
|
|
195
|
+
<TableColumn>ACTIONS</TableColumn>
|
|
207
196
|
</TableHeader>
|
|
208
197
|
<TableBody>
|
|
209
198
|
{users.map((user) => {
|
|
210
|
-
const
|
|
211
|
-
user.profile?.first_name && user.profile?.last_name
|
|
212
|
-
? `${user.profile.first_name} ${user.profile.last_name}`
|
|
213
|
-
: user.full_name || "N/A";
|
|
214
|
-
|
|
215
|
-
const roleColor =
|
|
216
|
-
user.role === "admin" || user.role === "superadmin"
|
|
217
|
-
? "danger"
|
|
218
|
-
: user.role === "moderator"
|
|
219
|
-
? "secondary"
|
|
220
|
-
: "default";
|
|
199
|
+
const displayName = user.full_name || user.email;
|
|
221
200
|
|
|
222
201
|
return (
|
|
223
202
|
<TableRow key={user.id}>
|
|
224
203
|
<TableCell>
|
|
225
204
|
<div className="flex items-center gap-2">
|
|
226
205
|
<Avatar
|
|
227
|
-
src={
|
|
228
|
-
|
|
206
|
+
src={
|
|
207
|
+
user.avatar_url
|
|
208
|
+
? `/api/storage/${user.avatar_url}`
|
|
209
|
+
: undefined
|
|
210
|
+
}
|
|
211
|
+
name={displayName}
|
|
229
212
|
size="sm"
|
|
230
213
|
/>
|
|
231
214
|
<span className="text-small font-medium">
|
|
232
|
-
{
|
|
215
|
+
{displayName}
|
|
233
216
|
</span>
|
|
234
217
|
</div>
|
|
235
218
|
</TableCell>
|
|
236
219
|
<TableCell>
|
|
237
220
|
<span className="text-small">{user.email}</span>
|
|
238
221
|
</TableCell>
|
|
239
|
-
<TableCell>
|
|
240
|
-
<Chip color={roleColor} size="sm" variant="flat">
|
|
241
|
-
{user.role || "user"}
|
|
242
|
-
</Chip>
|
|
243
|
-
</TableCell>
|
|
244
|
-
<TableCell>
|
|
245
|
-
<span className="text-small">
|
|
246
|
-
{user.profile?.company || "-"}
|
|
247
|
-
</span>
|
|
248
|
-
</TableCell>
|
|
249
222
|
<TableCell>
|
|
250
223
|
<span className="text-small">
|
|
251
224
|
{user.last_sign_in_at
|
|
252
225
|
? formatDate(user.last_sign_in_at)
|
|
253
|
-
: "
|
|
226
|
+
: "Jamais"}
|
|
254
227
|
</span>
|
|
255
228
|
</TableCell>
|
|
256
229
|
<TableCell>
|
|
@@ -259,9 +232,15 @@ export function AdminUsersPage() {
|
|
|
259
232
|
</span>
|
|
260
233
|
</TableCell>
|
|
261
234
|
<TableCell>
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
235
|
+
<Button
|
|
236
|
+
size="sm"
|
|
237
|
+
variant="flat"
|
|
238
|
+
color="primary"
|
|
239
|
+
onPress={() => handleViewUser(user.id)}
|
|
240
|
+
startContent={<Eye size={14} />}
|
|
241
|
+
>
|
|
242
|
+
Voir
|
|
243
|
+
</Button>
|
|
265
244
|
</TableCell>
|
|
266
245
|
</TableRow>
|
|
267
246
|
);
|