@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.
@@ -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,iBAuJtB,CAAC;AAEF,eAAe,eAAe,CAAC"}
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
@@ -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;AAEtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,wBAAwB,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,6 @@
1
+ interface UserDetailPageProps {
2
+ userId: string;
3
+ }
4
+ export declare function UserDetailPage({ userId }: UserDetailPageProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
6
+ //# sourceMappingURL=user-detail.d.ts.map
@@ -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,8 @@
1
+ interface UserPageProps {
2
+ params: Promise<{
3
+ id: string;
4
+ }>;
5
+ }
6
+ export default function UserPage({ params }: UserPageProps): Promise<import("react/jsx-runtime").JSX.Element>;
7
+ export {};
8
+ //# sourceMappingURL=%5Bid%5D.d.ts.map
@@ -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"}
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { UserDetailPage } from "../user-detail.js";
3
+ export default async function UserPage({ params }) {
4
+ const { id } = await params;
5
+ return _jsx(UserDetailPage, { userId: id });
6
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"AAsDA,wBAAgB,cAAc,4CA6O7B"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"AAuCA,wBAAgB,cAAc,4CAuO7B"}
@@ -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, Chip, Input, Button, Pagination, Avatar, addToast, } from "@lastbrain/ui";
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
- throw new Error("Failed to fetch users");
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
- setUsers(result.data || []);
42
- if (result.pagination) {
43
- setPagination(result.pagination);
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
- addToast({
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
- fetchUsers();
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("en-US", {
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: "ROLE" }), _jsx(TableColumn, { children: "COMPANY" }), _jsx(TableColumn, { children: "LAST SIGN IN" }), _jsx(TableColumn, { children: "CREATED" }), _jsx(TableColumn, { children: "STATUS" })] }), _jsx(TableBody, { children: users.map((user) => {
84
- const fullName = user.profile?.first_name && user.profile?.last_name
85
- ? `${user.profile.first_name} ${user.profile.last_name}`
86
- : user.full_name || "N/A";
87
- const roleColor = user.role === "admin" || user.role === "superadmin"
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
- : "Never" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: formatDate(user.created_at) }) }), _jsx(TableCell, { children: _jsx(Chip, { color: "success", size: "sm", variant: "flat", children: "Active" }) })] }, user.id));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/module-auth",
3
- "version": "0.1.6",
3
+ "version": "0.1.9",
4
4
  "description": "Module d'authentification complet pour LastBrain avec Supabase",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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
+ }