@lastbrain/module-auth 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/web/admin/user-detail.d.ts.map +1 -1
- package/dist/web/admin/user-detail.js +27 -6
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +6 -6
- package/package.json +1 -1
- package/src/web/admin/user-detail.tsx +244 -51
- package/src/web/admin/users.tsx +21 -3
- package/supabase/migrations/20251112000000_user_init.sql +6 -1
- package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +6 -1
- package/supabase/migrations/20251124000001_add_get_admin_user_details.sql +1 -0
package/README.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-detail.d.ts","sourceRoot":"","sources":["../../../src/web/admin/user-detail.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"user-detail.d.ts","sourceRoot":"","sources":["../../../src/web/admin/user-detail.tsx"],"names":[],"mappings":"AAyBA,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAiCD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CAClC;AAED,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,cAAmB,GACpB,EAAE,mBAAmB,2CA0frB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
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";
|
|
4
|
+
import { Card, CardHeader, CardBody, Tabs, Tab, Avatar, Chip, Button, Input, Textarea, Select, SelectItem, Spinner, addToast, Snippet, } from "@lastbrain/ui";
|
|
5
5
|
import { User, Bell, Settings } from "lucide-react";
|
|
6
6
|
import { useAuth } from "@lastbrain/core";
|
|
7
7
|
import * as LucideIcons from "lucide-react";
|
|
@@ -97,11 +97,32 @@ export function UserDetailPage({ userId, moduleUserTabs = [], }) {
|
|
|
97
97
|
}
|
|
98
98
|
const isAdmin = Array.isArray(userProfile.raw_app_meta_data?.roles) &&
|
|
99
99
|
userProfile.raw_app_meta_data.roles.includes("admin");
|
|
100
|
-
return (_jsxs("div", { className: "mt-4 space-y-6", children: [_jsx(Card, { children: _jsxs(CardHeader, { className: "flex
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
return (_jsxs("div", { className: "max-w-[calc(100vw-2rem)] mt-4 space-y-6", children: [_jsx(Card, { children: _jsxs(CardHeader, { className: "flex flex-col md:flex-row gap-4", children: [_jsx(Avatar, { isBordered: true, src: userProfile.avatar_sizes?.large || userProfile.avatar_url
|
|
101
|
+
? `/api/storage/${userProfile.avatar_sizes?.large || userProfile.avatar_url}`
|
|
102
|
+
: undefined, name: userProfile.full_name || userProfile.email, size: "lg" }), _jsxs("div", { className: "w-full flex flex-col gap-1", children: [_jsxs("div", { className: "w-full flex flex-col md:flex-row justify-between", children: [_jsx("h1", { className: "text-xl font-bold", children: userProfile.full_name || userProfile.email }), _jsxs("p", { className: "text-sm text-default-500 ", children: [_jsx("span", { className: "", children: "Derni\u00E8re connexion:" }), " ", userProfile.last_sign_in_at
|
|
103
|
+
? new Date(userProfile.last_sign_in_at).toLocaleDateString()
|
|
104
|
+
: "N/A", " ", "\u00E0", " ", userProfile.last_sign_in_at
|
|
105
|
+
? new Date(userProfile.last_sign_in_at).toLocaleTimeString()
|
|
106
|
+
: "N/A"] })] }), _jsx("p", { className: "text-gray-500", children: userProfile.email }), _jsxs("div", { className: "flex flex-col md:flex-row md:items-center gap-2", children: [_jsx(Chip, { variant: "flat", color: isAdmin ? "danger" : "primary", size: "sm", children: isAdmin ? "Administrateur" : "Utilisateur" }), _jsx(Snippet, { color: "default", size: "sm", symbol: "#", children: 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: _jsxs("div", { className: "space-y-6 mt-4", children: [_jsx(Card, { children: _jsxs(CardBody, { children: [_jsxs("h3", { className: "font-semibold text-lg mb-4 flex items-center gap-2", children: [_jsx(User, { size: 18 }), "Identit\u00E9"] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 text-sm", children: [_jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Nom complet" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.full_name || "Non renseigné" })] }), userProfile.profile?.first_name && (_jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Pr\u00E9nom" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.profile.first_name })] })), userProfile.profile?.last_name && (_jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Nom" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.profile.last_name })] })), _jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Email" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.email })] }), userProfile.profile?.phone && (_jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "T\u00E9l\u00E9phone" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.profile.phone })] }))] }), userProfile.profile?.bio && (_jsxs("div", { className: "mt-4", children: [_jsx("span", { className: "text-default-500 text-sm", children: "Bio" }), _jsx("p", { className: "font-medium mt-1 text-sm", children: userProfile.profile.bio })] }))] }) }), (userProfile.profile?.company ||
|
|
107
|
+
userProfile.profile?.website ||
|
|
108
|
+
userProfile.profile?.location) && (_jsx(Card, { children: _jsxs(CardBody, { children: [_jsx("h3", { className: "font-semibold text-lg mb-4", children: "Informations professionnelles" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 text-sm", children: [userProfile.profile?.company && (_jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Entreprise" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.profile.company })] })), userProfile.profile?.website && (_jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Site web" }), _jsx("p", { className: "font-medium mt-1", children: _jsx("a", { href: userProfile.profile.website, target: "_blank", rel: "noopener noreferrer", className: "text-primary hover:underline", children: userProfile.profile.website }) })] })), userProfile.profile?.location && (_jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Localisation" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.profile.location })] }))] })] }) })), (userProfile.profile?.language ||
|
|
109
|
+
userProfile.profile?.timezone) && (_jsx(Card, { children: _jsxs(CardBody, { children: [_jsx("h3", { className: "font-semibold text-lg mb-4", children: "Pr\u00E9f\u00E9rences" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 text-sm", children: [userProfile.profile?.language && (_jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Langue" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.profile.language.toUpperCase() })] })), userProfile.profile?.timezone && (_jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Fuseau horaire" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.profile.timezone })] }))] })] }) })), _jsx(Card, { children: _jsxs(CardBody, { children: [_jsx("h3", { className: "font-semibold text-lg mb-4", children: "Informations du compte" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 text-sm", children: [_jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "R\u00F4le" }), _jsx("div", { className: "mt-2", children: _jsx(Chip, { variant: "flat", color: isAdmin ? "danger" : "default", size: "sm", children: isAdmin
|
|
110
|
+
? "Administrateur"
|
|
111
|
+
: "Utilisateur standard" }) })] }), _jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Date de cr\u00E9ation" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.created_at
|
|
112
|
+
? new Date(userProfile.created_at).toLocaleDateString("fr-FR", {
|
|
113
|
+
year: "numeric",
|
|
114
|
+
month: "long",
|
|
115
|
+
day: "numeric",
|
|
116
|
+
})
|
|
117
|
+
: "N/A" })] }), _jsxs("div", { children: [_jsx("span", { className: "text-default-500", children: "Derni\u00E8re connexion" }), _jsx("p", { className: "font-medium mt-1", children: userProfile.last_sign_in_at
|
|
118
|
+
? new Date(userProfile.last_sign_in_at).toLocaleDateString("fr-FR", {
|
|
119
|
+
year: "numeric",
|
|
120
|
+
month: "long",
|
|
121
|
+
day: "numeric",
|
|
122
|
+
hour: "2-digit",
|
|
123
|
+
minute: "2-digit",
|
|
124
|
+
})
|
|
125
|
+
: "Jamais" })] })] })] }) })] }) }, "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({
|
|
105
126
|
app_metadata: userProfile.raw_app_meta_data,
|
|
106
127
|
user_metadata: userProfile.raw_user_meta_data,
|
|
107
128
|
}, null, 2) })] })] }) }, "settings"), moduleUserTabs.map((tab) => {
|
|
@@ -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":"AA6CA,wBAAgB,cAAc,4CAmP7B"}
|
package/dist/web/admin/users.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
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 {
|
|
4
|
+
import { Card, CardBody, CardHeader, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Spinner, Chip, Input, Button, Pagination, Avatar, } from "@lastbrain/ui";
|
|
5
|
+
import { Search, RefreshCw, Eye, Users2 } from "lucide-react";
|
|
6
6
|
import { useRouter } from "next/navigation";
|
|
7
7
|
export function AdminUsersPage() {
|
|
8
8
|
const router = useRouter();
|
|
@@ -76,15 +76,15 @@ 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(
|
|
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(Users2, { 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: "LAST SIGN IN" }), _jsx(TableColumn, { children: "CREATED" }), _jsx(TableColumn, { children: "ACTIONS" })] }), _jsx(TableBody, { children: users.map((user) => {
|
|
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, { isStriped: true, "aria-label": "Users table", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: "USER" }), _jsx(TableColumn, { children: "EMAIL" }), _jsx(TableColumn, { children: "ROLE" }), _jsx(TableColumn, { children: "LAST SIGN IN" }), _jsx(TableColumn, { children: "CREATED" }), _jsx(TableColumn, { children: "ACTIONS" })] }), _jsx(TableBody, { children: users.map((user) => {
|
|
84
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
|
|
85
|
+
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Avatar, { isBordered: true, src: user.avatar_url
|
|
86
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
|
|
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(Chip, { size: "sm", variant: "flat", color: user.role === "admin" ? "danger" : "default", children: user.role || "user" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.last_sign_in_at
|
|
88
88
|
? formatDate(user.last_sign_in_at)
|
|
89
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));
|
|
90
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"] })] })) })] })] }));
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
Spacer,
|
|
18
18
|
Spinner,
|
|
19
19
|
addToast,
|
|
20
|
+
Snippet,
|
|
20
21
|
} from "@lastbrain/ui";
|
|
21
22
|
import { User, Bell, Settings, Save } from "lucide-react";
|
|
22
23
|
import { useAuth } from "@lastbrain/core";
|
|
@@ -38,6 +39,26 @@ interface UserProfile {
|
|
|
38
39
|
last_sign_in_at: string;
|
|
39
40
|
raw_app_meta_data: Record<string, unknown>;
|
|
40
41
|
raw_user_meta_data: Record<string, unknown>;
|
|
42
|
+
avatar_sizes?: {
|
|
43
|
+
small?: string;
|
|
44
|
+
medium?: string;
|
|
45
|
+
large?: string;
|
|
46
|
+
};
|
|
47
|
+
profile?: {
|
|
48
|
+
first_name?: string;
|
|
49
|
+
last_name?: string;
|
|
50
|
+
bio?: string;
|
|
51
|
+
phone?: string;
|
|
52
|
+
company?: string;
|
|
53
|
+
website?: string;
|
|
54
|
+
location?: string;
|
|
55
|
+
language?: string;
|
|
56
|
+
timezone?: string;
|
|
57
|
+
preferences?: Record<string, unknown>;
|
|
58
|
+
avatar_url?: string;
|
|
59
|
+
created_at?: string;
|
|
60
|
+
updated_at?: string;
|
|
61
|
+
};
|
|
41
62
|
}
|
|
42
63
|
|
|
43
64
|
interface UserDetailPageProps {
|
|
@@ -163,27 +184,48 @@ export function UserDetailPage({
|
|
|
163
184
|
userProfile.raw_app_meta_data.roles.includes("admin");
|
|
164
185
|
|
|
165
186
|
return (
|
|
166
|
-
<div className="mt-4 space-y-6">
|
|
187
|
+
<div className="max-w-[calc(100vw-2rem)] mt-4 space-y-6">
|
|
167
188
|
{/* Header utilisateur */}
|
|
168
189
|
<Card>
|
|
169
|
-
<CardHeader className="flex gap-4">
|
|
190
|
+
<CardHeader className="flex flex-col md:flex-row gap-4">
|
|
170
191
|
<Avatar
|
|
171
|
-
|
|
192
|
+
isBordered
|
|
193
|
+
src={
|
|
194
|
+
userProfile.avatar_sizes?.large || userProfile.avatar_url
|
|
195
|
+
? `/api/storage/${userProfile.avatar_sizes?.large || userProfile.avatar_url}`
|
|
196
|
+
: undefined
|
|
197
|
+
}
|
|
172
198
|
name={userProfile.full_name || userProfile.email}
|
|
173
199
|
size="lg"
|
|
174
200
|
/>
|
|
175
|
-
<div className="flex flex-col gap-1">
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
201
|
+
<div className="w-full flex flex-col gap-1">
|
|
202
|
+
<div className="w-full flex flex-col md:flex-row justify-between">
|
|
203
|
+
<h1 className="text-xl font-bold">
|
|
204
|
+
{userProfile.full_name || userProfile.email}
|
|
205
|
+
</h1>
|
|
206
|
+
<p className="text-sm text-default-500 ">
|
|
207
|
+
<span className="">Dernière connexion:</span>{" "}
|
|
208
|
+
{userProfile.last_sign_in_at
|
|
209
|
+
? new Date(userProfile.last_sign_in_at).toLocaleDateString()
|
|
210
|
+
: "N/A"}{" "}
|
|
211
|
+
à{" "}
|
|
212
|
+
{userProfile.last_sign_in_at
|
|
213
|
+
? new Date(userProfile.last_sign_in_at).toLocaleTimeString()
|
|
214
|
+
: "N/A"}
|
|
215
|
+
</p>
|
|
216
|
+
</div>
|
|
179
217
|
<p className="text-gray-500">{userProfile.email}</p>
|
|
180
|
-
<div className="flex gap-2">
|
|
181
|
-
<Chip
|
|
218
|
+
<div className="flex flex-col md:flex-row md:items-center gap-2">
|
|
219
|
+
<Chip
|
|
220
|
+
variant="flat"
|
|
221
|
+
color={isAdmin ? "danger" : "primary"}
|
|
222
|
+
size="sm"
|
|
223
|
+
>
|
|
182
224
|
{isAdmin ? "Administrateur" : "Utilisateur"}
|
|
183
225
|
</Chip>
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
</
|
|
226
|
+
<Snippet color="default" size="sm" symbol="#">
|
|
227
|
+
{userId}
|
|
228
|
+
</Snippet>
|
|
187
229
|
</div>
|
|
188
230
|
</div>
|
|
189
231
|
</CardHeader>
|
|
@@ -209,48 +251,199 @@ export function UserDetailPage({
|
|
|
209
251
|
</div>
|
|
210
252
|
}
|
|
211
253
|
>
|
|
212
|
-
<div className="space-y-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
254
|
+
<div className="space-y-6 mt-4">
|
|
255
|
+
{/* Section: Identité */}
|
|
256
|
+
<Card>
|
|
257
|
+
<CardBody>
|
|
258
|
+
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
259
|
+
<User size={18} />
|
|
260
|
+
Identité
|
|
217
261
|
</h3>
|
|
218
|
-
<div className="
|
|
219
|
-
<
|
|
220
|
-
<span className="
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
262
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
263
|
+
<div>
|
|
264
|
+
<span className="text-default-500">Nom complet</span>
|
|
265
|
+
<p className="font-medium mt-1">
|
|
266
|
+
{userProfile.full_name || "Non renseigné"}
|
|
267
|
+
</p>
|
|
268
|
+
</div>
|
|
269
|
+
{userProfile.profile?.first_name && (
|
|
270
|
+
<div>
|
|
271
|
+
<span className="text-default-500">Prénom</span>
|
|
272
|
+
<p className="font-medium mt-1">
|
|
273
|
+
{userProfile.profile.first_name}
|
|
274
|
+
</p>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
{userProfile.profile?.last_name && (
|
|
278
|
+
<div>
|
|
279
|
+
<span className="text-default-500">Nom</span>
|
|
280
|
+
<p className="font-medium mt-1">
|
|
281
|
+
{userProfile.profile.last_name}
|
|
282
|
+
</p>
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
<div>
|
|
286
|
+
<span className="text-default-500">Email</span>
|
|
287
|
+
<p className="font-medium mt-1">{userProfile.email}</p>
|
|
288
|
+
</div>
|
|
289
|
+
{userProfile.profile?.phone && (
|
|
290
|
+
<div>
|
|
291
|
+
<span className="text-default-500">Téléphone</span>
|
|
292
|
+
<p className="font-medium mt-1">
|
|
293
|
+
{userProfile.profile.phone}
|
|
294
|
+
</p>
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
243
297
|
</div>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
</
|
|
298
|
+
{userProfile.profile?.bio && (
|
|
299
|
+
<div className="mt-4">
|
|
300
|
+
<span className="text-default-500 text-sm">Bio</span>
|
|
301
|
+
<p className="font-medium mt-1 text-sm">
|
|
302
|
+
{userProfile.profile.bio}
|
|
303
|
+
</p>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</CardBody>
|
|
307
|
+
</Card>
|
|
308
|
+
|
|
309
|
+
{/* Section: Informations professionnelles */}
|
|
310
|
+
{(userProfile.profile?.company ||
|
|
311
|
+
userProfile.profile?.website ||
|
|
312
|
+
userProfile.profile?.location) && (
|
|
313
|
+
<Card>
|
|
314
|
+
<CardBody>
|
|
315
|
+
<h3 className="font-semibold text-lg mb-4">
|
|
316
|
+
Informations professionnelles
|
|
317
|
+
</h3>
|
|
318
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
319
|
+
{userProfile.profile?.company && (
|
|
320
|
+
<div>
|
|
321
|
+
<span className="text-default-500">Entreprise</span>
|
|
322
|
+
<p className="font-medium mt-1">
|
|
323
|
+
{userProfile.profile.company}
|
|
324
|
+
</p>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
{userProfile.profile?.website && (
|
|
328
|
+
<div>
|
|
329
|
+
<span className="text-default-500">Site web</span>
|
|
330
|
+
<p className="font-medium mt-1">
|
|
331
|
+
<a
|
|
332
|
+
href={userProfile.profile.website}
|
|
333
|
+
target="_blank"
|
|
334
|
+
rel="noopener noreferrer"
|
|
335
|
+
className="text-primary hover:underline"
|
|
336
|
+
>
|
|
337
|
+
{userProfile.profile.website}
|
|
338
|
+
</a>
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
{userProfile.profile?.location && (
|
|
343
|
+
<div>
|
|
344
|
+
<span className="text-default-500">
|
|
345
|
+
Localisation
|
|
346
|
+
</span>
|
|
347
|
+
<p className="font-medium mt-1">
|
|
348
|
+
{userProfile.profile.location}
|
|
349
|
+
</p>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
</CardBody>
|
|
354
|
+
</Card>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{/* Section: Préférences */}
|
|
358
|
+
{(userProfile.profile?.language ||
|
|
359
|
+
userProfile.profile?.timezone) && (
|
|
360
|
+
<Card>
|
|
361
|
+
<CardBody>
|
|
362
|
+
<h3 className="font-semibold text-lg mb-4">
|
|
363
|
+
Préférences
|
|
364
|
+
</h3>
|
|
365
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
366
|
+
{userProfile.profile?.language && (
|
|
367
|
+
<div>
|
|
368
|
+
<span className="text-default-500">Langue</span>
|
|
369
|
+
<p className="font-medium mt-1">
|
|
370
|
+
{userProfile.profile.language.toUpperCase()}
|
|
371
|
+
</p>
|
|
372
|
+
</div>
|
|
373
|
+
)}
|
|
374
|
+
{userProfile.profile?.timezone && (
|
|
375
|
+
<div>
|
|
376
|
+
<span className="text-default-500">
|
|
377
|
+
Fuseau horaire
|
|
378
|
+
</span>
|
|
379
|
+
<p className="font-medium mt-1">
|
|
380
|
+
{userProfile.profile.timezone}
|
|
381
|
+
</p>
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
385
|
+
</CardBody>
|
|
386
|
+
</Card>
|
|
387
|
+
)}
|
|
388
|
+
|
|
389
|
+
{/* Section: Compte */}
|
|
390
|
+
<Card>
|
|
391
|
+
<CardBody>
|
|
392
|
+
<h3 className="font-semibold text-lg mb-4">
|
|
393
|
+
Informations du compte
|
|
394
|
+
</h3>
|
|
395
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
396
|
+
<div>
|
|
397
|
+
<span className="text-default-500">Rôle</span>
|
|
398
|
+
<div className="mt-2">
|
|
399
|
+
<Chip
|
|
400
|
+
variant="flat"
|
|
401
|
+
color={isAdmin ? "danger" : "default"}
|
|
402
|
+
size="sm"
|
|
403
|
+
>
|
|
404
|
+
{isAdmin
|
|
405
|
+
? "Administrateur"
|
|
406
|
+
: "Utilisateur standard"}
|
|
407
|
+
</Chip>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
<div>
|
|
411
|
+
<span className="text-default-500">
|
|
412
|
+
Date de création
|
|
413
|
+
</span>
|
|
414
|
+
<p className="font-medium mt-1">
|
|
415
|
+
{userProfile.created_at
|
|
416
|
+
? new Date(
|
|
417
|
+
userProfile.created_at,
|
|
418
|
+
).toLocaleDateString("fr-FR", {
|
|
419
|
+
year: "numeric",
|
|
420
|
+
month: "long",
|
|
421
|
+
day: "numeric",
|
|
422
|
+
})
|
|
423
|
+
: "N/A"}
|
|
424
|
+
</p>
|
|
425
|
+
</div>
|
|
426
|
+
<div>
|
|
427
|
+
<span className="text-default-500">
|
|
428
|
+
Dernière connexion
|
|
429
|
+
</span>
|
|
430
|
+
<p className="font-medium mt-1">
|
|
431
|
+
{userProfile.last_sign_in_at
|
|
432
|
+
? new Date(
|
|
433
|
+
userProfile.last_sign_in_at,
|
|
434
|
+
).toLocaleDateString("fr-FR", {
|
|
435
|
+
year: "numeric",
|
|
436
|
+
month: "long",
|
|
437
|
+
day: "numeric",
|
|
438
|
+
hour: "2-digit",
|
|
439
|
+
minute: "2-digit",
|
|
440
|
+
})
|
|
441
|
+
: "Jamais"}
|
|
442
|
+
</p>
|
|
443
|
+
</div>
|
|
251
444
|
</div>
|
|
252
|
-
</
|
|
253
|
-
</
|
|
445
|
+
</CardBody>
|
|
446
|
+
</Card>
|
|
254
447
|
</div>
|
|
255
448
|
</Tab>
|
|
256
449
|
|
package/src/web/admin/users.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
Pagination,
|
|
19
19
|
Avatar,
|
|
20
20
|
} from "@lastbrain/ui";
|
|
21
|
-
import { Users, Search, RefreshCw, Eye } from "lucide-react";
|
|
21
|
+
import { Users, Search, RefreshCw, Eye, Users2 } from "lucide-react";
|
|
22
22
|
import { useRouter } from "next/navigation";
|
|
23
23
|
|
|
24
24
|
interface User {
|
|
@@ -28,6 +28,12 @@ interface User {
|
|
|
28
28
|
last_sign_in_at?: string;
|
|
29
29
|
full_name?: string;
|
|
30
30
|
avatar_url?: string;
|
|
31
|
+
avatar_sizes?: {
|
|
32
|
+
small?: string;
|
|
33
|
+
medium?: string;
|
|
34
|
+
large?: string;
|
|
35
|
+
};
|
|
36
|
+
role?: string;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
interface PaginationData {
|
|
@@ -40,6 +46,7 @@ interface PaginationData {
|
|
|
40
46
|
export function AdminUsersPage() {
|
|
41
47
|
const router = useRouter();
|
|
42
48
|
const searchInputId = useId();
|
|
49
|
+
|
|
43
50
|
const [users, setUsers] = useState<User[]>([]);
|
|
44
51
|
const [isLoading, setIsLoading] = useState(true);
|
|
45
52
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -136,7 +143,7 @@ export function AdminUsersPage() {
|
|
|
136
143
|
return (
|
|
137
144
|
<div className="pt-12 pb-12 max-w-7xl mx-auto px-4">
|
|
138
145
|
<div className="flex items-center gap-2 mb-8">
|
|
139
|
-
<
|
|
146
|
+
<Users2 className="w-8 h-8" />
|
|
140
147
|
<h1 className="text-3xl font-bold">User Management</h1>
|
|
141
148
|
</div>
|
|
142
149
|
|
|
@@ -186,10 +193,11 @@ export function AdminUsersPage() {
|
|
|
186
193
|
</div>
|
|
187
194
|
) : (
|
|
188
195
|
<>
|
|
189
|
-
<Table aria-label="Users table">
|
|
196
|
+
<Table isStriped aria-label="Users table">
|
|
190
197
|
<TableHeader>
|
|
191
198
|
<TableColumn>USER</TableColumn>
|
|
192
199
|
<TableColumn>EMAIL</TableColumn>
|
|
200
|
+
<TableColumn>ROLE</TableColumn>
|
|
193
201
|
<TableColumn>LAST SIGN IN</TableColumn>
|
|
194
202
|
<TableColumn>CREATED</TableColumn>
|
|
195
203
|
<TableColumn>ACTIONS</TableColumn>
|
|
@@ -203,6 +211,7 @@ export function AdminUsersPage() {
|
|
|
203
211
|
<TableCell>
|
|
204
212
|
<div className="flex items-center gap-2">
|
|
205
213
|
<Avatar
|
|
214
|
+
isBordered
|
|
206
215
|
src={
|
|
207
216
|
user.avatar_url
|
|
208
217
|
? `/api/storage/${user.avatar_url}`
|
|
@@ -219,6 +228,15 @@ export function AdminUsersPage() {
|
|
|
219
228
|
<TableCell>
|
|
220
229
|
<span className="text-small">{user.email}</span>
|
|
221
230
|
</TableCell>
|
|
231
|
+
<TableCell>
|
|
232
|
+
<Chip
|
|
233
|
+
size="sm"
|
|
234
|
+
variant="flat"
|
|
235
|
+
color={user.role === "admin" ? "danger" : "default"}
|
|
236
|
+
>
|
|
237
|
+
{user.role || "user"}
|
|
238
|
+
</Chip>
|
|
239
|
+
</TableCell>
|
|
222
240
|
<TableCell>
|
|
223
241
|
<span className="text-small">
|
|
224
242
|
{user.last_sign_in_at
|
|
@@ -172,4 +172,9 @@ DROP TRIGGER IF EXISTS set_user_notifications_updated_at ON public.user_notifica
|
|
|
172
172
|
CREATE TRIGGER set_user_notifications_updated_at
|
|
173
173
|
BEFORE UPDATE ON public.user_notifications
|
|
174
174
|
FOR EACH ROW
|
|
175
|
-
EXECUTE FUNCTION public.set_user_notifications_updated_at();
|
|
175
|
+
EXECUTE FUNCTION public.set_user_notifications_updated_at();
|
|
176
|
+
|
|
177
|
+
-- =====================================================
|
|
178
|
+
-- Enable Realtime for user_notifications
|
|
179
|
+
-- =====================================================
|
|
180
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.user_notifications;
|
|
@@ -88,7 +88,12 @@ BEGIN
|
|
|
88
88
|
'last_sign_in_at', au.last_sign_in_at,
|
|
89
89
|
'role', COALESCE(au.raw_app_meta_data->'roles'->>0, 'user'),
|
|
90
90
|
'full_name', au.raw_user_meta_data->>'full_name',
|
|
91
|
-
'
|
|
91
|
+
'avatar_url', COALESCE(
|
|
92
|
+
au.raw_user_meta_data->'avatar_sizes'->>'large',
|
|
93
|
+
au.raw_user_meta_data->>'avatar',
|
|
94
|
+
fp.avatar_url
|
|
95
|
+
),
|
|
96
|
+
'avatar_sizes', au.raw_user_meta_data->'avatar_sizes',
|
|
92
97
|
'metadata', au.raw_user_meta_data,
|
|
93
98
|
'profile', json_build_object(
|
|
94
99
|
'first_name', fp.first_name,
|
|
@@ -33,6 +33,7 @@ BEGIN
|
|
|
33
33
|
'avatar_url', COALESCE(p.avatar_url, au.raw_user_meta_data->>'avatar'),
|
|
34
34
|
'raw_app_meta_data', COALESCE(au.raw_app_meta_data, '{}'::jsonb),
|
|
35
35
|
'raw_user_meta_data', COALESCE(au.raw_user_meta_data, '{}'::jsonb),
|
|
36
|
+
'avatar_sizes', COALESCE(au.raw_user_meta_data->'avatar_sizes', '{}'::jsonb),
|
|
36
37
|
'profile', json_build_object(
|
|
37
38
|
'first_name', p.first_name,
|
|
38
39
|
'last_name', p.last_name,
|