@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 CHANGED
@@ -85,6 +85,7 @@
85
85
  - `20251112000001_auto_profile_and_admin_view.sql`
86
86
  - `20251112000002_sync_avatars.sql`
87
87
  - `20251124000001_add_get_admin_user_details.sql`
88
+ - `20251127100000_rename_body_to_message.sql`
88
89
 
89
90
  ## 📦 Installation
90
91
 
@@ -1 +1 @@
1
- {"version":3,"file":"user-detail.d.ts","sourceRoot":"","sources":["../../../src/web/admin/user-detail.tsx"],"names":[],"mappings":"AAwBA,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;AAaD,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,2CA8UrB"}
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 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
101
- ? new Date(userProfile.created_at).toLocaleDateString()
102
- : "N/A"] }), _jsxs("p", { children: [_jsx("span", { className: "font-medium", children: "Derni\u00E8re connexion:" }), " ", userProfile.last_sign_in_at
103
- ? new Date(userProfile.last_sign_in_at).toLocaleDateString()
104
- : "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({
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":"AAuCA,wBAAgB,cAAc,4CAuO7B"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"AA6CA,wBAAgB,cAAc,4CAmP7B"}
@@ -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 { Users, Search, RefreshCw, Eye } from "lucide-react";
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(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) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/module-auth",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Module d'authentification complet pour LastBrain avec Supabase",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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
- src={userProfile.avatar_url}
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
- <h1 className="text-2xl font-bold">
177
- {userProfile.full_name || userProfile.email}
178
- </h1>
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 color={isAdmin ? "danger" : "primary"} size="sm">
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
- <Chip color="default" size="sm">
185
- ID: {userId}
186
- </Chip>
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-4 mt-4">
213
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
214
- <div>
215
- <h3 className="font-semibold mb-2">
216
- Informations personnelles
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="space-y-2 text-sm">
219
- <p>
220
- <span className="font-medium">Nom complet:</span>{" "}
221
- {userProfile.full_name || "Non renseigné"}
222
- </p>
223
- <p>
224
- <span className="font-medium">Email:</span>{" "}
225
- {userProfile.email}
226
- </p>
227
- <p>
228
- <span className="font-medium">Date de création:</span>{" "}
229
- {userProfile.created_at
230
- ? new Date(
231
- userProfile.created_at,
232
- ).toLocaleDateString()
233
- : "N/A"}
234
- </p>
235
- <p>
236
- <span className="font-medium">Dernière connexion:</span>{" "}
237
- {userProfile.last_sign_in_at
238
- ? new Date(
239
- userProfile.last_sign_in_at,
240
- ).toLocaleDateString()
241
- : "N/A"}
242
- </p>
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
- </div>
245
- <div>
246
- <h3 className="font-semibold mb-2">Rôles et permissions</h3>
247
- <div className="space-y-2">
248
- <Chip color={isAdmin ? "danger" : "default"}>
249
- {isAdmin ? "Administrateur" : "Utilisateur standard"}
250
- </Chip>
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
- </div>
253
- </div>
445
+ </CardBody>
446
+ </Card>
254
447
  </div>
255
448
  </Tab>
256
449
 
@@ -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
- <Users className="w-8 h-8" />
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
- 'avatar_path', au.raw_user_meta_data->>'avatar',
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,