@lastbrain/module-auth 2.0.27 → 2.0.31
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 +55 -7
- package/dist/api/admin/signup-stats.d.ts.map +1 -1
- package/dist/api/admin/signup-stats.js +2 -1
- package/dist/api/admin/storage/usage.d.ts +18 -0
- package/dist/api/admin/storage/usage.d.ts.map +1 -0
- package/dist/api/admin/storage/usage.js +100 -0
- package/dist/api/admin/users/[id]/notifications.d.ts.map +1 -1
- package/dist/api/admin/users/[id]/notifications.js +3 -2
- package/dist/api/admin/users/[id].d.ts.map +1 -1
- package/dist/api/admin/users/[id].js +3 -2
- package/dist/api/admin/users/reactivate/[id].d.ts +16 -0
- package/dist/api/admin/users/reactivate/[id].d.ts.map +1 -0
- package/dist/api/admin/users/reactivate/[id].js +59 -0
- package/dist/api/admin/users/suspend/[id].d.ts +16 -0
- package/dist/api/admin/users/suspend/[id].d.ts.map +1 -0
- package/dist/api/admin/users/suspend/[id].js +59 -0
- package/dist/api/admin/users-by-source.d.ts.map +1 -1
- package/dist/api/admin/users-by-source.js +2 -1
- package/dist/api/admin/users.d.ts.map +1 -1
- package/dist/api/admin/users.js +53 -2
- package/dist/api/auth/account/email-change.d.ts +7 -0
- package/dist/api/auth/account/email-change.d.ts.map +1 -0
- package/dist/api/auth/account/email-change.js +39 -0
- package/dist/api/auth/account/reset-password.d.ts +7 -0
- package/dist/api/auth/account/reset-password.d.ts.map +1 -0
- package/dist/api/auth/account/reset-password.js +36 -0
- package/dist/api/auth/check-username.d.ts +9 -0
- package/dist/api/auth/check-username.d.ts.map +1 -0
- package/dist/api/auth/check-username.js +35 -0
- package/dist/api/auth/establish-session.d.ts +2 -0
- package/dist/api/auth/establish-session.d.ts.map +1 -0
- package/dist/api/auth/establish-session.js +23 -0
- package/dist/api/auth/me.d.ts +4 -4
- package/dist/api/auth/me.d.ts.map +1 -1
- package/dist/api/auth/me.js +28 -6
- package/dist/api/auth/profile.d.ts.map +1 -1
- package/dist/api/auth/profile.js +6 -3
- package/dist/api/auth/storage/recalculate.d.ts +15 -0
- package/dist/api/auth/storage/recalculate.d.ts.map +1 -0
- package/dist/api/auth/storage/recalculate.js +68 -0
- package/dist/api/auth/storage/usage.d.ts +10 -0
- package/dist/api/auth/storage/usage.d.ts.map +1 -0
- package/dist/api/auth/storage/usage.js +86 -0
- package/dist/api/public/add-welcome-bonus.d.ts +16 -0
- package/dist/api/public/add-welcome-bonus.d.ts.map +1 -0
- package/dist/api/public/add-welcome-bonus.js +177 -0
- package/dist/api/public/callback.d.ts +3 -0
- package/dist/api/public/callback.d.ts.map +1 -0
- package/dist/api/public/callback.js +197 -0
- package/dist/api/public/reset-password.d.ts +3 -0
- package/dist/api/public/reset-password.d.ts.map +1 -0
- package/dist/api/public/reset-password.js +43 -0
- package/dist/api/public/set-session.d.ts +7 -0
- package/dist/api/public/set-session.d.ts.map +1 -0
- package/dist/api/public/set-session.js +55 -0
- package/dist/api/public/signin.d.ts.map +1 -1
- package/dist/api/public/signin.js +31 -0
- package/dist/api/public/signup.d.ts.map +1 -1
- package/dist/api/public/signup.js +38 -27
- package/dist/api/public/webhook/storage-addon.d.ts +9 -0
- package/dist/api/public/webhook/storage-addon.d.ts.map +1 -0
- package/dist/api/public/webhook/storage-addon.js +155 -0
- package/dist/api/storage.js +2 -2
- package/dist/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +126 -11
- package/dist/components/AccountButton.d.ts.map +1 -1
- package/dist/components/AccountButton.js +54 -28
- package/dist/components/Doc.d.ts.map +1 -1
- package/dist/components/Doc.js +1 -1
- package/dist/components/HasProfil.d.ts +4 -0
- package/dist/components/HasProfil.d.ts.map +1 -0
- package/dist/components/HasProfil.js +39 -0
- package/dist/components/auth/dashboard.d.ts +1 -1
- package/dist/components/auth/dashboard.d.ts.map +1 -1
- package/dist/components/auth/dashboard.js +34 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/app-branding-data.d.ts +22 -0
- package/dist/lib/app-branding-data.d.ts.map +1 -0
- package/dist/lib/app-branding-data.js +49 -0
- package/dist/lib/auth-email-service.d.ts +57 -0
- package/dist/lib/auth-email-service.d.ts.map +1 -0
- package/dist/lib/auth-email-service.js +382 -0
- package/dist/lib/auth-email-templates.d.ts +2 -0
- package/dist/lib/auth-email-templates.d.ts.map +1 -0
- package/dist/lib/auth-email-templates.js +1 -0
- package/dist/lib/site-url.d.ts +3 -0
- package/dist/lib/site-url.d.ts.map +1 -0
- package/dist/lib/site-url.js +11 -0
- package/dist/sitemap/manifest.d.ts +9 -0
- package/dist/sitemap/manifest.d.ts.map +1 -0
- package/dist/sitemap/manifest.js +14 -0
- package/dist/web/admin/signup-stats.js +3 -3
- package/dist/web/admin/user-detail.d.ts.map +1 -1
- package/dist/web/admin/user-detail.js +135 -14
- package/dist/web/admin/users-by-signup-source.js +2 -2
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +26 -7
- package/dist/web/auth/folder.d.ts.map +1 -1
- package/dist/web/auth/folder.js +4 -3
- package/dist/web/auth/profile.d.ts.map +1 -1
- package/dist/web/auth/profile.js +132 -13
- package/dist/web/auth/reglage.d.ts.map +1 -1
- package/dist/web/auth/reglage.js +15 -8
- package/dist/web/public/ResetPassword.d.ts.map +1 -1
- package/dist/web/public/ResetPassword.js +172 -2
- package/dist/web/public/SignInPage.d.ts.map +1 -1
- package/dist/web/public/SignInPage.js +39 -3
- package/dist/web/public/SignUpPage.d.ts.map +1 -1
- package/dist/web/public/SignUpPage.js +7 -2
- package/dist/web/public/auth-code-error.d.ts +2 -0
- package/dist/web/public/auth-code-error.d.ts.map +1 -0
- package/dist/web/public/auth-code-error.js +14 -0
- package/dist/web/public/confirm.d.ts +2 -0
- package/dist/web/public/confirm.d.ts.map +1 -0
- package/dist/web/public/confirm.js +157 -0
- package/package.json +10 -5
- package/src/api/admin/signup-stats.ts +2 -1
- package/src/api/admin/storage/usage.ts +141 -0
- package/src/api/admin/users/[id]/notifications.ts +3 -2
- package/src/api/admin/users/[id].ts +3 -2
- package/src/api/admin/users/reactivate/[id].ts +88 -0
- package/src/api/admin/users/suspend/[id].ts +85 -0
- package/src/api/admin/users-by-source.ts +2 -1
- package/src/api/admin/users.ts +59 -2
- package/src/api/auth/account/email-change.ts +54 -0
- package/src/api/auth/account/reset-password.ts +47 -0
- package/src/api/auth/check-username.ts +52 -0
- package/src/api/auth/establish-session.ts +32 -0
- package/src/api/auth/me.ts +29 -7
- package/src/api/auth/profile.ts +6 -2
- package/src/api/auth/storage/recalculate.ts +108 -0
- package/src/api/auth/storage/usage.ts +113 -0
- package/src/api/public/add-welcome-bonus.ts +229 -0
- package/src/api/public/callback.ts +307 -0
- package/src/api/public/reset-password.ts +52 -0
- package/src/api/public/set-session.ts +73 -0
- package/src/api/public/signin.ts +36 -0
- package/src/api/public/signup.ts +44 -37
- package/src/api/public/webhook/storage-addon.ts +267 -0
- package/src/api/storage.ts +2 -2
- package/src/auth.build.config.ts +126 -11
- package/src/components/AccountButton.tsx +114 -90
- package/src/components/Doc.tsx +47 -9
- package/src/components/HasProfil.tsx +63 -0
- package/src/components/auth/dashboard.tsx +54 -13
- package/src/i18n/en.json +76 -8
- package/src/i18n/es.json +330 -0
- package/src/i18n/fr.json +74 -8
- package/src/index.ts +2 -0
- package/src/lib/app-branding-data.ts +90 -0
- package/src/lib/auth-email-service.ts +508 -0
- package/src/lib/auth-email-templates.ts +5 -0
- package/src/lib/site-url.ts +17 -0
- package/src/sitemap/manifest.ts +26 -0
- package/src/web/admin/signup-stats.tsx +3 -3
- package/src/web/admin/user-detail.tsx +314 -15
- package/src/web/admin/users-by-signup-source.tsx +2 -2
- package/src/web/admin/users.tsx +50 -14
- package/src/web/auth/folder.tsx +23 -5
- package/src/web/auth/profile.tsx +227 -13
- package/src/web/auth/reglage.tsx +55 -24
- package/src/web/public/ResetPassword.tsx +301 -1
- package/src/web/public/SignInPage.tsx +43 -3
- package/src/web/public/SignUpPage.tsx +14 -5
- package/src/web/public/auth-code-error.tsx +49 -0
- package/src/web/public/confirm.tsx +195 -0
- package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +3 -1
- package/supabase/migrations/20251127100000_rename_body_to_message.sql +8 -3
- package/supabase/migrations/20260120150001_add_user_storage.sql +105 -0
- package/supabase/migrations/20260122131200_add_global_addons_system.sql +305 -0
- package/supabase/migrations/20260123100000_enable_vector_extension.sql +9 -0
- package/supabase/migrations/20260123140000_module_auth_fix_get_user_limits_null.sql +93 -0
- package/supabase/migrations/20260123145000_module_auth_fix_circular_storage_base.sql +89 -0
- package/supabase/migrations/20260129400000_add_username_to_user_profil.sql +33 -0
- package/dist/web/auth/dashboard.d.ts +0 -2
- package/dist/web/auth/dashboard.d.ts.map +0 -1
- package/dist/web/auth/dashboard.js +0 -48
|
@@ -1,9 +1,9 @@
|
|
|
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 { useModuleTranslation, useLanguage } from "@lastbrain/core";
|
|
5
|
-
import { Card, CardHeader, CardBody, Tabs, Tab, Avatar, Chip, Button, Input, Textarea, Select, SelectItem, Spinner, addToast, Snippet, } from "@lastbrain/ui";
|
|
6
|
-
import { User, Bell, Settings } from "lucide-react";
|
|
4
|
+
import { useModuleTranslation, useLanguage, logger } from "@lastbrain/core";
|
|
5
|
+
import { Card, CardHeader, CardBody, Tabs, Tab, Avatar, Chip, Button, Input, Textarea, Select, SelectItem, Spinner, addToast, Snippet, StorageUsageCard, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, } from "@lastbrain/ui";
|
|
6
|
+
import { User, Bell, Settings, HardDrive, Package } from "lucide-react";
|
|
7
7
|
import { useAuth } from "@lastbrain/core";
|
|
8
8
|
import * as LucideIcons from "lucide-react";
|
|
9
9
|
export function UserDetailPage({ userId, moduleUserTabs = [], }) {
|
|
@@ -17,6 +17,26 @@ export function UserDetailPage({ userId, moduleUserTabs = [], }) {
|
|
|
17
17
|
const [notificationMessage, setNotificationMessage] = useState("");
|
|
18
18
|
const [notificationType, setNotificationType] = useState("info");
|
|
19
19
|
const [sendingNotification, setSendingNotification] = useState(false);
|
|
20
|
+
const [storageAddons, setStorageAddons] = useState([]);
|
|
21
|
+
const [loadingAddons, setLoadingAddons] = useState(false);
|
|
22
|
+
// Fonction pour charger les addons de stockage de l'utilisateur
|
|
23
|
+
const fetchStorageAddons = useCallback(async () => {
|
|
24
|
+
try {
|
|
25
|
+
setLoadingAddons(true);
|
|
26
|
+
const response = await fetch(`/api/admin/billing/user-addons?userId=${userId}`);
|
|
27
|
+
if (response.ok) {
|
|
28
|
+
const data = await response.json();
|
|
29
|
+
const storageAddonsOnly = (data.addons || []).filter((addon) => addon.addon_type === "storage");
|
|
30
|
+
setStorageAddons(storageAddonsOnly);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
logger.error("Error fetching storage addons:", err);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setLoadingAddons(false);
|
|
38
|
+
}
|
|
39
|
+
}, [userId]);
|
|
20
40
|
// Fonction pour charger les données de l'utilisateur
|
|
21
41
|
const fetchUserProfile = useCallback(async () => {
|
|
22
42
|
try {
|
|
@@ -25,7 +45,7 @@ export function UserDetailPage({ userId, moduleUserTabs = [], }) {
|
|
|
25
45
|
const response = await fetch(`/api/admin/users/${userId}`);
|
|
26
46
|
if (!response.ok) {
|
|
27
47
|
if (response.status === 404) {
|
|
28
|
-
|
|
48
|
+
logger.error("Utilisateur non trouvé");
|
|
29
49
|
setUserProfile(null);
|
|
30
50
|
return;
|
|
31
51
|
}
|
|
@@ -36,7 +56,7 @@ export function UserDetailPage({ userId, moduleUserTabs = [], }) {
|
|
|
36
56
|
setUserProfile(userDetails);
|
|
37
57
|
}
|
|
38
58
|
catch (error) {
|
|
39
|
-
|
|
59
|
+
logger.error(t("user_detail.profile_loading_error") ||
|
|
40
60
|
"Erreur lors du chargement du profil:", error);
|
|
41
61
|
setUserProfile(null);
|
|
42
62
|
}
|
|
@@ -47,7 +67,8 @@ export function UserDetailPage({ userId, moduleUserTabs = [], }) {
|
|
|
47
67
|
// Charger les données de l'utilisateur
|
|
48
68
|
useEffect(() => {
|
|
49
69
|
fetchUserProfile();
|
|
50
|
-
|
|
70
|
+
fetchStorageAddons();
|
|
71
|
+
}, [fetchUserProfile, fetchStorageAddons]);
|
|
51
72
|
const handleSendNotification = async () => {
|
|
52
73
|
if (!notificationTitle.trim() || !notificationMessage.trim()) {
|
|
53
74
|
addToast({
|
|
@@ -96,15 +117,101 @@ export function UserDetailPage({ userId, moduleUserTabs = [], }) {
|
|
|
96
117
|
setSendingNotification(false);
|
|
97
118
|
}
|
|
98
119
|
};
|
|
120
|
+
const handleResetPassword = async () => {
|
|
121
|
+
try {
|
|
122
|
+
if (!userProfile?.email) {
|
|
123
|
+
addToast({
|
|
124
|
+
color: "danger",
|
|
125
|
+
title: t("user_detail.no_email") || "Email introuvable",
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const resp = await fetch(`/api/auth/account/reset-password`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: { "Content-Type": "application/json" },
|
|
132
|
+
body: JSON.stringify({ email: userProfile.email }),
|
|
133
|
+
});
|
|
134
|
+
if (!resp.ok) {
|
|
135
|
+
const err = await resp.json().catch(() => ({}));
|
|
136
|
+
throw new Error(err?.error || "Erreur lors de l'envoi du lien de réinitialisation");
|
|
137
|
+
}
|
|
138
|
+
addToast({
|
|
139
|
+
color: "success",
|
|
140
|
+
title: t("user_detail.reset_password_sent") ||
|
|
141
|
+
"Lien de réinitialisation envoyé",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
addToast({
|
|
146
|
+
color: "danger",
|
|
147
|
+
title: t("user_detail.reset_password_error") || (err?.message ?? "Erreur"),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const handleSuspendAccount = async () => {
|
|
152
|
+
if (!confirm(t("user_detail.suspend_confirm") ||
|
|
153
|
+
"Confirmer la suspension du compte ?"))
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
const resp = await fetch(`/api/admin/users/suspend/${userId}`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
});
|
|
159
|
+
if (!resp.ok) {
|
|
160
|
+
const err = await resp.json().catch(() => ({}));
|
|
161
|
+
throw new Error(err?.error || "Failed to suspend user");
|
|
162
|
+
}
|
|
163
|
+
addToast({
|
|
164
|
+
color: "success",
|
|
165
|
+
title: t("user_detail.suspend_success") || "Compte suspendu",
|
|
166
|
+
});
|
|
167
|
+
// refresh profile
|
|
168
|
+
fetchUserProfile();
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
addToast({
|
|
172
|
+
color: "danger",
|
|
173
|
+
title: t("user_detail.suspend_error") || (err?.message ?? "Erreur"),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const handleReactivateAccount = async () => {
|
|
178
|
+
if (!confirm(t("user_detail.reactivate_confirm") ||
|
|
179
|
+
"Confirmer la réactivation du compte ?"))
|
|
180
|
+
return;
|
|
181
|
+
try {
|
|
182
|
+
const resp = await fetch(`/api/admin/users/reactivate/${userId}`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
});
|
|
185
|
+
if (!resp.ok) {
|
|
186
|
+
const err = await resp.json().catch(() => ({}));
|
|
187
|
+
throw new Error(err?.error || "Failed to reactivate user");
|
|
188
|
+
}
|
|
189
|
+
addToast({
|
|
190
|
+
color: "success",
|
|
191
|
+
title: t("user_detail.reactivate_success") || "Compte réactivé",
|
|
192
|
+
});
|
|
193
|
+
// refresh profile
|
|
194
|
+
fetchUserProfile();
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
addToast({
|
|
198
|
+
color: "danger",
|
|
199
|
+
title: t("user_detail.reactivate_error") || (err?.message ?? "Erreur"),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
};
|
|
99
203
|
if (loading) {
|
|
100
|
-
return (_jsx("div", { className: "flex justify-center items-center min-h-64", children: _jsx(Spinner, { size: "lg" }) }));
|
|
204
|
+
return (_jsx("div", { className: "mt-16 flex justify-center items-center min-h-64", children: _jsx(Spinner, { size: "lg" }) }));
|
|
101
205
|
}
|
|
102
206
|
if (!userProfile) {
|
|
103
207
|
return (_jsx(Card, { children: _jsx(CardBody, { children: _jsx("p", { className: "text-center text-gray-500", children: t("user_detail.user_not_found") || "Utilisateur non trouvé" }) }) }));
|
|
104
208
|
}
|
|
105
209
|
const isAdmin = Array.isArray(userProfile.raw_app_meta_data?.roles) &&
|
|
106
210
|
userProfile.raw_app_meta_data.roles.includes("admin");
|
|
107
|
-
|
|
211
|
+
const isSuspended = Boolean(userProfile.raw_app_meta_data?.suspended ||
|
|
212
|
+
userProfile.raw_app_meta_data?.app_metadata?.suspended ||
|
|
213
|
+
userProfile.app_metadata?.suspended);
|
|
214
|
+
return (_jsxs("div", { className: "mt-16 max-w-7xl mx-auto mt-4 space-y-6 pt-12 ", 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
|
|
108
215
|
? `/api/storage/${userProfile.avatar_sizes?.large || userProfile.avatar_url}`
|
|
109
216
|
: 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: t("user_detail.last_login") || "Dernière connexion:" }), " ", userProfile.last_sign_in_at
|
|
110
217
|
? new Date(userProfile.last_sign_in_at).toLocaleDateString()
|
|
@@ -112,7 +219,7 @@ export function UserDetailPage({ userId, moduleUserTabs = [], }) {
|
|
|
112
219
|
? new Date(userProfile.last_sign_in_at).toLocaleTimeString()
|
|
113
220
|
: t("user_detail.not_available") || "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
|
|
114
221
|
? t("user_detail.administrator") || "Administrateur"
|
|
115
|
-
: t("user_detail.user") || "Utilisateur" }), userProfile.profile?.signup_source && (_jsx(Chip, { variant: "flat", color: userProfile.profile.signup_source.toLowerCase() === "recipe"
|
|
222
|
+
: t("user_detail.user") || "Utilisateur" }), isSuspended ? (_jsx(Chip, { variant: "flat", color: "danger", size: "sm", children: t("user_detail.suspended") || "Suspendu" })) : (_jsx(Chip, { variant: "flat", color: "success", size: "sm", children: t("user_detail.active") || "Actif" })), userProfile.profile?.signup_source && (_jsx(Chip, { variant: "flat", color: userProfile.profile.signup_source.toLowerCase() === "recipe"
|
|
116
223
|
? "success"
|
|
117
224
|
: "secondary", size: "sm", children: userProfile.profile.signup_source.toLowerCase() === "recipe"
|
|
118
225
|
? t("user_detail.source_recipe") || "Recipe"
|
|
@@ -147,14 +254,28 @@ export function UserDetailPage({ userId, moduleUserTabs = [], }) {
|
|
|
147
254
|
"Ex: Nouveau message important", value: notificationTitle, onChange: (e) => setNotificationTitle(e.target.value), maxLength: 100 }), _jsx(Textarea, { label: t("user_detail.notification_message") || "Message", placeholder: t("user_detail.notification_message_placeholder") ||
|
|
148
255
|
"Contenu de la notification...", value: notificationMessage, onChange: (e) => setNotificationMessage(e.target.value), maxLength: 500, minRows: 3 }), _jsxs(Select, { label: t("user_detail.notification_type") ||
|
|
149
256
|
"Type de notification", selectedKeys: [notificationType], onSelectionChange: (keys) => setNotificationType(Array.from(keys)[0]), children: [_jsx(SelectItem, { children: t("user_detail.notification_type_info") || "Information" }, "info"), _jsx(SelectItem, { children: t("user_detail.notification_type_warning") ||
|
|
150
|
-
"Avertissement" }, "warning"), _jsx(SelectItem, { children: t("user_detail.notification_type_danger") || "Danger" }, "danger"), _jsx(SelectItem, { children: t("user_detail.notification_type_success") || "Succès" }, "success")] }), _jsx(Button, { color: "primary", onPress: handleSendNotification, isLoading: sendingNotification, startContent: _jsx(Bell, { size: 16 }), isDisabled: !notificationTitle.trim() || !notificationMessage.trim(), children: t("user_detail.send_button") || "Envoyer la notification" })] })] }) }, "notifications"), _jsx(Tab, { title: _jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(Settings, { size: 16 }), _jsx("span", { children: t("user_detail.tab_settings") || "Paramètres" })] }), children: _jsxs("div", { className: "space-y-4 mt-4", children: [_jsx("h3", { className: "font-semibold", children: t("user_detail.admin_actions") || "Actions administrateur" }), _jsxs("div", { className: "space-y-3 space-x-5", children: [_jsx(Button, { color: "warning", variant: "bordered", size: "sm", children: t("user_detail.reset_password_btn") ||
|
|
151
|
-
"Réinitialiser le mot de passe" }), _jsx(Button, { color: "
|
|
152
|
-
"
|
|
153
|
-
"
|
|
257
|
+
"Avertissement" }, "warning"), _jsx(SelectItem, { children: t("user_detail.notification_type_danger") || "Danger" }, "danger"), _jsx(SelectItem, { children: t("user_detail.notification_type_success") || "Succès" }, "success")] }), _jsx(Button, { color: "primary", onPress: handleSendNotification, isLoading: sendingNotification, startContent: _jsx(Bell, { size: 16 }), isDisabled: !notificationTitle.trim() || !notificationMessage.trim(), children: t("user_detail.send_button") || "Envoyer la notification" })] })] }) }, "notifications"), _jsx(Tab, { title: _jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(Settings, { size: 16 }), _jsx("span", { children: t("user_detail.tab_settings") || "Paramètres" })] }), children: _jsxs("div", { className: "space-y-4 mt-4", children: [_jsx("h3", { className: "font-semibold", children: t("user_detail.admin_actions") || "Actions administrateur" }), _jsxs("div", { className: "space-y-3 space-x-5", children: [_jsx(Button, { color: "warning", variant: "bordered", size: "sm", onPress: handleResetPassword, children: t("user_detail.reset_password_btn") ||
|
|
258
|
+
"Réinitialiser le mot de passe" }), isSuspended ? (_jsx(Button, { color: "success", variant: "bordered", size: "sm", onPress: handleReactivateAccount, children: t("user_detail.reactivate_account_btn") ||
|
|
259
|
+
"Réactiver le compte" })) : (_jsx(Button, { color: "danger", variant: "bordered", size: "sm", onPress: handleSuspendAccount, children: t("user_detail.suspend_account_btn") ||
|
|
260
|
+
"Suspendre le compte" }))] }), _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: t("user_detail.technical_metadata") ||
|
|
154
261
|
"Métadonnées techniques" }), _jsx("pre", { className: "text-xs text-gray-600 dark:text-gray-400 overflow-auto", children: JSON.stringify({
|
|
155
262
|
app_metadata: userProfile.raw_app_meta_data,
|
|
156
263
|
user_metadata: userProfile.raw_user_meta_data,
|
|
157
|
-
}, null, 2) })] })] }) }, "settings"),
|
|
264
|
+
}, null, 2) })] })] }) }, "settings"), _jsx(Tab, { title: _jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(HardDrive, { size: 16 }), _jsx("span", { children: t("user_detail.tab_storage") || "Stockage" })] }), 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(HardDrive, { size: 18 }), t("user_detail.storage_usage") || "Usage du stockage"] }), _jsx(StorageUsageCard, { ownerId: userId, title: "", showDetails: true, apiEndpoint: "/api/admin/storage/usage" })] }) }), _jsx(Card, { children: _jsxs(CardBody, { children: [_jsxs("h3", { className: "font-semibold text-lg mb-4 flex items-center gap-2", children: [_jsx(Package, { size: 18 }), t("user_detail.storage_addons") ||
|
|
265
|
+
"Addons de stockage actifs"] }), loadingAddons ? (_jsx("div", { className: "flex justify-center py-8", children: _jsx(Spinner, { size: "md" }) })) : storageAddons.length === 0 ? (_jsx("div", { className: "text-center py-8 text-default-500", children: t("user_detail.no_storage_addons") ||
|
|
266
|
+
"Aucun addon de stockage actif" })) : (_jsxs(Table, { "aria-label": "Storage addons", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: "NOM" }), _jsx(TableColumn, { children: "QUOTA" }), _jsx(TableColumn, { children: "QUANTIT\u00C9" }), _jsx(TableColumn, { children: "STATUT" }), _jsx(TableColumn, { children: "DATE D'AJOUT" })] }), _jsx(TableBody, { children: storageAddons.map((addon) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: addon.name }), addon.description && (_jsx("div", { className: "text-xs text-default-500", children: addon.description }))] }) }), _jsxs(TableCell, { children: [addon.quota_value, " ", addon.quota_unit] }), _jsx(TableCell, { children: _jsxs(Chip, { size: "sm", variant: "flat", children: ["\u00D7 ", addon.quantity] }) }), _jsx(TableCell, { children: _jsx(Chip, { size: "sm", color: addon.status === "active"
|
|
267
|
+
? "success"
|
|
268
|
+
: addon.status === "cancelled"
|
|
269
|
+
? "warning"
|
|
270
|
+
: "default", variant: "flat", children: addon.status === "active"
|
|
271
|
+
? "Actif"
|
|
272
|
+
: addon.status === "cancelled"
|
|
273
|
+
? "Résilié"
|
|
274
|
+
: addon.status }) }), _jsx(TableCell, { children: new Date(addon.created_at).toLocaleDateString("fr-FR", {
|
|
275
|
+
year: "numeric",
|
|
276
|
+
month: "short",
|
|
277
|
+
day: "numeric",
|
|
278
|
+
}) })] }, addon.id))) })] })), storageAddons.length > 0 && (_jsx("div", { className: "mt-4 p-3 bg-primary-50 dark:bg-primary-900/20 rounded-lg", children: _jsxs("p", { className: "text-sm text-primary-700 dark:text-primary-300", children: [_jsx("strong", { children: "Total addons :" }), " ", storageAddons.reduce((sum, addon) => sum + addon.quota_value * addon.quantity, 0), " ", "GB additionnels"] }) }))] }) })] }) }, "storage"), moduleUserTabs.map((tab) => {
|
|
158
279
|
const TabComponent = tab.component;
|
|
159
280
|
const IconComponent = tab.icon
|
|
160
281
|
? LucideIcons[tab.icon]
|
|
@@ -68,9 +68,9 @@ export function UsersBySignupSourcePage() {
|
|
|
68
68
|
return src.toLowerCase() === "recipe" ? "success" : "secondary";
|
|
69
69
|
};
|
|
70
70
|
if (error) {
|
|
71
|
-
return (_jsx("div", { className: "p-6", children: _jsx(Card, { className: "border border-danger-200 bg-danger-50/50", children: _jsx(CardBody, { children: _jsx("p", { className: "text-danger-600", children: error }) }) }) }));
|
|
71
|
+
return (_jsx("div", { className: "mt-16 p-6", children: _jsx(Card, { className: "border border-danger-200 bg-danger-50/50", children: _jsx(CardBody, { children: _jsx("p", { className: "text-danger-600", children: error }) }) }) }));
|
|
72
72
|
}
|
|
73
|
-
return (_jsxs("div", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-8", children: [_jsx(Users, { size: 28, className: "text-primary-600" }), _jsx("h1", { className: "text-3xl font-bold", children: t("users_by_source.title") ||
|
|
73
|
+
return (_jsxs("div", { className: "mt-16 space-y-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-8", children: [_jsx(Users, { size: 28, className: "text-primary-600" }), _jsx("h1", { className: "text-3xl font-bold", children: t("users_by_source.title") ||
|
|
74
74
|
"Utilisateurs par source d'inscription" })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsx(Input, { placeholder: t("users_by_source.search_placeholder") ||
|
|
75
75
|
"Rechercher par email ou nom...", value: searchQuery, onChange: handleSearch, startContent: _jsx(Search, { size: 16 }), isClearable: true, onClear: () => {
|
|
76
76
|
setSearchQuery("");
|
|
@@ -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":"AAkDA,wBAAgB,cAAc,4CAyS7B"}
|
package/dist/web/admin/users.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { useCallback, useEffect, useState, useId } from "react";
|
|
4
4
|
import { Card, CardBody, CardHeader, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Spinner, Chip, Input, Button, Pagination, Avatar, } from "@lastbrain/ui";
|
|
5
5
|
import { Search, RefreshCw, Eye, Users2 } from "lucide-react";
|
|
6
|
-
import { useModuleTranslation } from "@lastbrain/core";
|
|
6
|
+
import { logger, useModuleTranslation } from "@lastbrain/core";
|
|
7
7
|
import { useLocalizedRouter } from "@lastbrain/core";
|
|
8
8
|
export function AdminUsersPage() {
|
|
9
9
|
const router = useLocalizedRouter();
|
|
@@ -50,7 +50,7 @@ export function AdminUsersPage() {
|
|
|
50
50
|
}
|
|
51
51
|
catch (err) {
|
|
52
52
|
setError(err instanceof Error ? err.message : "An error occurred");
|
|
53
|
-
|
|
53
|
+
logger.error("Erreur lors du chargement des utilisateurs:", err);
|
|
54
54
|
}
|
|
55
55
|
finally {
|
|
56
56
|
setIsLoading(false);
|
|
@@ -76,19 +76,38 @@ export function AdminUsersPage() {
|
|
|
76
76
|
});
|
|
77
77
|
};
|
|
78
78
|
if (error && users.length === 0) {
|
|
79
|
-
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 }) }) }) }));
|
|
79
|
+
return (_jsx("div", { className: "mt-16 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 }) }) }) }));
|
|
80
80
|
}
|
|
81
|
-
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: t("users.title") || "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: t("users.search_placeholder") || "Search by email or name...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onKeyPress: (e) => {
|
|
81
|
+
return (_jsxs("div", { className: "mt-16 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: t("users.title") || "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: t("users.search_placeholder") || "Search by email or name...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onKeyPress: (e) => {
|
|
82
82
|
if (e.key === "Enter") {
|
|
83
83
|
handleSearch();
|
|
84
84
|
}
|
|
85
|
-
}, startContent: _jsx(Search, { className: "w-4 h-4 text-default-400" }), className: "flex-1" }), _jsx(Button, { color: "primary", onPress: handleSearch, isDisabled: isLoading, children: t("users.search_button") || "Search" })] }), _jsx(Button, { variant: "flat", onPress: fetchUsers, isDisabled: isLoading, startContent: _jsx(RefreshCw, { className: "w-4 h-4" }), children: t("users.refresh_button") || "Refresh" })] }) }), _jsx(CardBody, { children: isLoading ? (_jsx("div", { className: "flex justify-center items-center py-12", children: _jsx(Spinner, { size: "lg", label: t("users.loading_users") || "Loading users..." }) })) : users.length === 0 ? (_jsx("div", { className: "text-center py-12 text-default-500", children: t("users.no_users_found") || "No users found" })) : (_jsxs(_Fragment, { children: [_jsxs(Table, { isStriped: true, "aria-label": t("users.table_aria_label") || "Users table", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: t("users.column_user") || "USER" }), _jsx(TableColumn, { children: t("users.column_email") || "EMAIL" }), _jsx(TableColumn, { children: t("users.column_role") || "ROLE" }), _jsx(TableColumn, { children: t("users.column_last_sign_in") || "LAST SIGN IN" }), _jsx(TableColumn, { children: t("users.column_created") || "CREATED" }), _jsx(TableColumn, { children: t("users.column_actions") || "ACTIONS" })] }), _jsx(TableBody, { children: users.map((user) => {
|
|
85
|
+
}, startContent: _jsx(Search, { className: "w-4 h-4 text-default-400" }), className: "flex-1" }), _jsx(Button, { color: "primary", onPress: handleSearch, isDisabled: isLoading, children: t("users.search_button") || "Search" })] }), _jsx(Button, { variant: "flat", onPress: fetchUsers, isDisabled: isLoading, startContent: _jsx(RefreshCw, { className: "w-4 h-4" }), children: t("users.refresh_button") || "Refresh" })] }) }), _jsx(CardBody, { children: isLoading ? (_jsx("div", { className: "flex justify-center items-center py-12", children: _jsx(Spinner, { size: "lg", label: t("users.loading_users") || "Loading users..." }) })) : users.length === 0 ? (_jsx("div", { className: "text-center py-12 text-default-500", children: t("users.no_users_found") || "No users found" })) : (_jsxs(_Fragment, { children: [_jsxs(Table, { isStriped: true, "aria-label": t("users.table_aria_label") || "Users table", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: t("users.column_user") || "USER" }), _jsx(TableColumn, { children: t("users.column_email") || "EMAIL" }), _jsx(TableColumn, { children: t("users.column_role") || "ROLE" }), _jsx(TableColumn, { children: "STATUT" }), _jsx(TableColumn, { children: t("users.column_last_sign_in") || "LAST SIGN IN" }), _jsx(TableColumn, { children: t("users.column_created") || "CREATED" }), _jsx(TableColumn, { align: "end", children: t("users.column_actions") || "ACTIONS" })] }), _jsx(TableBody, { children: users.map((user) => {
|
|
86
86
|
const displayName = user.full_name || user.email;
|
|
87
|
+
const isSuspended = (() => {
|
|
88
|
+
const u = user;
|
|
89
|
+
// check multiple possible locations returned by different RPCs
|
|
90
|
+
if (u.app_metadata?.suspended)
|
|
91
|
+
return true;
|
|
92
|
+
if (u.raw_app_meta_data?.suspended)
|
|
93
|
+
return true;
|
|
94
|
+
if (u.raw_app_meta_data?.app_metadata?.suspended)
|
|
95
|
+
return true;
|
|
96
|
+
// some RPCs may wrap app metadata twice
|
|
97
|
+
if (u.raw_app_meta_data?.raw_app_meta_data?.suspended)
|
|
98
|
+
return true;
|
|
99
|
+
if (u.raw_app_meta_data?.raw_app_meta_data?.app_metadata
|
|
100
|
+
?.suspended)
|
|
101
|
+
return true;
|
|
102
|
+
return false;
|
|
103
|
+
})();
|
|
87
104
|
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Avatar, { isBordered: true, src: user.avatar_url
|
|
88
105
|
? `/api/storage/${user.avatar_url}`
|
|
89
|
-
: 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("
|
|
106
|
+
: 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(Chip, { size: "sm", variant: "flat", color: isSuspended ? "danger" : "success", children: isSuspended
|
|
107
|
+
? t("user_detail.suspended") || "Suspendu"
|
|
108
|
+
: t("user_detail.active") || "Actif" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.last_sign_in_at
|
|
90
109
|
? formatDate(user.last_sign_in_at)
|
|
91
|
-
: t("users.never") || "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: t("users.view_button") || "Voir" }) })] }, user.id));
|
|
110
|
+
: t("users.never") || "Jamais" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: formatDate(user.created_at) }) }), _jsx(TableCell, { children: _jsx("div", { className: "flex justify-end", children: _jsx(Button, { size: "sm", variant: "flat", color: "primary", onPress: () => handleViewUser(user.id), startContent: _jsx(Eye, { size: 14 }), children: t("users.view_button") || "Voir" }) }) })] }, user.id));
|
|
92
111
|
}) })] }), 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 }) })), _jsx("div", { className: "mt-4 text-small text-default-500 text-center", children: t("users.showing_results")
|
|
93
112
|
?.replace("{{count}}", users.length.toString())
|
|
94
113
|
.replace("{{total}}", pagination.total.toString()) ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"folder.d.ts","sourceRoot":"","sources":["../../../src/web/auth/folder.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"folder.d.ts","sourceRoot":"","sources":["../../../src/web/auth/folder.tsx"],"names":[],"mappings":"AA2BA,wBAAgB,UAAU,mDAuFzB"}
|
package/dist/web/auth/folder.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { Card, CardBody, Spinner } from "@lastbrain/ui";
|
|
4
|
+
import { Alert, Card, CardBody, Spinner, StorageUsageCard, } from "@lastbrain/ui";
|
|
5
5
|
import { FileManager } from "@lastbrain/ui";
|
|
6
6
|
import { useModuleTranslation } from "@lastbrain/core";
|
|
7
7
|
export function FolderPage() {
|
|
@@ -15,7 +15,7 @@ export function FolderPage() {
|
|
|
15
15
|
const fetchUserData = async () => {
|
|
16
16
|
try {
|
|
17
17
|
setIsLoading(true);
|
|
18
|
-
const response = await fetch("/api/auth/me");
|
|
18
|
+
const response = await fetch("/api/auth/me", { credentials: "include" });
|
|
19
19
|
if (!response.ok) {
|
|
20
20
|
throw new Error("Failed to fetch user data");
|
|
21
21
|
}
|
|
@@ -38,5 +38,6 @@ export function FolderPage() {
|
|
|
38
38
|
if (!userData) {
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
|
-
return (_jsxs("div", { className: "pt-4 pb-12 max-w-8xl mx-auto px-4", children: [_jsx("h1", { className: "text-3xl font-bold
|
|
41
|
+
return (_jsxs("div", { className: "pt-4 pb-12 max-w-8xl mx-auto px-4", children: [_jsxs("div", { className: "flex justify-between items-center mb-4 gap-4", children: [_jsx("h1", { className: "text-3xl font-bold", children: t("folder.title") || "Dossier" }), _jsx(StorageUsageCard, { ownerId: userData.id, title: t("folder.storage_usage") || "Espace de stockage", showDetails: false, size: "sm" })] }), _jsx(FileManager, { bucket: "app", basePath: userData.id, allowUpload: true, allowCreateFolder: true, allowAIImageGeneration: false, className: "min-h-[80vh]" }), _jsx(Alert, { color: "primary", className: "mt-6", children: t("folder.note") ||
|
|
42
|
+
"Note : Cet espace regroupe les fichiers générés par vos différentes applications et modules. Tous les fichiers restent centralisés et accessibles depuis votre espace personnel." })] }));
|
|
42
43
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/web/auth/profile.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/web/auth/profile.tsx"],"names":[],"mappings":"AA2CA,wBAAgB,WAAW,4CAmnB1B"}
|
package/dist/web/auth/profile.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import { Card, CardBody, CardHeader, Input, Textarea, Button, Spinner, Divider, addToast, AvatarUploader, } from "@lastbrain/ui";
|
|
5
|
-
import { Save, User } from "lucide-react";
|
|
6
|
-
import { useModuleTranslation } from "@lastbrain/core";
|
|
5
|
+
import { Check, Save, User, XCircle } from "lucide-react";
|
|
6
|
+
import { logger, useModuleTranslation } from "@lastbrain/core";
|
|
7
7
|
import { uploadFile, deleteFilesWithPrefix } from "../../api/storage";
|
|
8
8
|
import { supabaseBrowserClient } from "@lastbrain/core";
|
|
9
9
|
export function ProfilePage() {
|
|
@@ -12,6 +12,10 @@ export function ProfilePage() {
|
|
|
12
12
|
const [isLoading, setIsLoading] = useState(true);
|
|
13
13
|
const [isSaving, setIsSaving] = useState(false);
|
|
14
14
|
const [_error, setError] = useState(null);
|
|
15
|
+
const [newEmail, setNewEmail] = useState("");
|
|
16
|
+
const [securityLoading, setSecurityLoading] = useState(false);
|
|
17
|
+
const [usernameAvailable, setUsernameAvailable] = useState(null);
|
|
18
|
+
const [checkingUsername, setCheckingUsername] = useState(false);
|
|
15
19
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
20
|
const [currentUser, setCurrentUser] = useState(null);
|
|
17
21
|
useEffect(() => {
|
|
@@ -24,13 +28,15 @@ export function ProfilePage() {
|
|
|
24
28
|
setCurrentUser(user);
|
|
25
29
|
}
|
|
26
30
|
catch (err) {
|
|
27
|
-
|
|
31
|
+
logger.error("Error fetching current user:", err);
|
|
28
32
|
}
|
|
29
33
|
};
|
|
30
34
|
const fetchProfile = async () => {
|
|
31
35
|
try {
|
|
32
36
|
setIsLoading(true);
|
|
33
|
-
const response = await fetch("/api/auth/profile"
|
|
37
|
+
const response = await fetch("/api/auth/profile", {
|
|
38
|
+
credentials: "include",
|
|
39
|
+
});
|
|
34
40
|
if (!response.ok) {
|
|
35
41
|
throw new Error("Failed to fetch profile");
|
|
36
42
|
}
|
|
@@ -60,6 +66,7 @@ export function ProfilePage() {
|
|
|
60
66
|
headers: {
|
|
61
67
|
"Content-Type": "application/json",
|
|
62
68
|
},
|
|
69
|
+
credentials: "include",
|
|
63
70
|
body: JSON.stringify(profile),
|
|
64
71
|
});
|
|
65
72
|
if (!response.ok) {
|
|
@@ -72,7 +79,7 @@ export function ProfilePage() {
|
|
|
72
79
|
});
|
|
73
80
|
}
|
|
74
81
|
catch (err) {
|
|
75
|
-
|
|
82
|
+
logger.error("Error updating profile:", err);
|
|
76
83
|
setError(err instanceof Error ? err.message : "An error occurred");
|
|
77
84
|
addToast({
|
|
78
85
|
title: t("profile.error") || "Error",
|
|
@@ -87,6 +94,104 @@ export function ProfilePage() {
|
|
|
87
94
|
const handleChange = (field, value) => {
|
|
88
95
|
setProfile((prev) => ({ ...prev, [field]: value }));
|
|
89
96
|
};
|
|
97
|
+
// Check username availability with debounce
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!profile.username?.trim()) {
|
|
100
|
+
setUsernameAvailable(null);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const timeoutId = setTimeout(async () => {
|
|
104
|
+
setCheckingUsername(true);
|
|
105
|
+
if (!profile.username) {
|
|
106
|
+
setUsernameAvailable(null);
|
|
107
|
+
setCheckingUsername(false);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const usernameLower = profile.username.toLowerCase();
|
|
112
|
+
const response = await fetch(`/api/auth/check-username?username=${encodeURIComponent(usernameLower)}&owner_id=${currentUser?.id || ""}`);
|
|
113
|
+
if (response.ok) {
|
|
114
|
+
const { available } = await response.json();
|
|
115
|
+
setUsernameAvailable(available);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
logger.error("Error checking username:", error);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
setCheckingUsername(false);
|
|
123
|
+
}
|
|
124
|
+
}, 500);
|
|
125
|
+
return () => clearTimeout(timeoutId);
|
|
126
|
+
}, [profile.username, currentUser?.id]);
|
|
127
|
+
const handleSendResetLink = async () => {
|
|
128
|
+
try {
|
|
129
|
+
setSecurityLoading(true);
|
|
130
|
+
const response = await fetch("/api/auth/account/reset-password", {
|
|
131
|
+
method: "POST",
|
|
132
|
+
});
|
|
133
|
+
const data = await response.json();
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw new Error(data.error || "Erreur lors de l'envoi du lien");
|
|
136
|
+
}
|
|
137
|
+
addToast({
|
|
138
|
+
title: t("profile.security_reset_sent") || "Lien envoyé",
|
|
139
|
+
description: t("profile.security_reset_desc") ||
|
|
140
|
+
"Vérifiez votre email pour réinitialiser votre mot de passe",
|
|
141
|
+
color: "success",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
addToast({
|
|
146
|
+
title: t("profile.security_error") || "Erreur",
|
|
147
|
+
description: error?.message || "Impossible d'envoyer le lien",
|
|
148
|
+
color: "danger",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
setSecurityLoading(false);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const handleEmailChangeRequest = async () => {
|
|
156
|
+
if (!newEmail) {
|
|
157
|
+
addToast({
|
|
158
|
+
title: t("profile.security_error") || "Erreur",
|
|
159
|
+
description: t("profile.security_email_required") || "Nouvel email requis",
|
|
160
|
+
color: "warning",
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
setSecurityLoading(true);
|
|
166
|
+
const response = await fetch("/api/auth/account/email-change", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: {
|
|
169
|
+
"Content-Type": "application/json",
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({ newEmail }),
|
|
172
|
+
});
|
|
173
|
+
const data = await response.json();
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
throw new Error(data.error || "Erreur lors de l'envoi du lien");
|
|
176
|
+
}
|
|
177
|
+
addToast({
|
|
178
|
+
title: t("profile.security_email_change_sent") || "Confirmation envoyée",
|
|
179
|
+
description: t("profile.security_email_change_desc") ||
|
|
180
|
+
"Vérifiez le nouvel email pour confirmer le changement",
|
|
181
|
+
color: "success",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
addToast({
|
|
186
|
+
title: t("profile.security_error") || "Erreur",
|
|
187
|
+
description: error?.message || "Impossible d'envoyer la confirmation",
|
|
188
|
+
color: "danger",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
setSecurityLoading(false);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
90
195
|
const handleAvatarUpload = async (files) => {
|
|
91
196
|
if (!currentUser)
|
|
92
197
|
throw new Error("User not authenticated");
|
|
@@ -118,16 +223,17 @@ export function ProfilePage() {
|
|
|
118
223
|
headers: {
|
|
119
224
|
"Content-Type": "application/json",
|
|
120
225
|
},
|
|
226
|
+
credentials: "include",
|
|
121
227
|
body: JSON.stringify({
|
|
122
228
|
avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
|
|
123
229
|
}),
|
|
124
230
|
});
|
|
125
231
|
if (!response.ok) {
|
|
126
|
-
|
|
232
|
+
logger.error("Failed to update avatar_url in profile");
|
|
127
233
|
}
|
|
128
234
|
}
|
|
129
235
|
catch (error) {
|
|
130
|
-
|
|
236
|
+
logger.error("Error updating profile avatar_url:", error);
|
|
131
237
|
}
|
|
132
238
|
// Update profile avatar_url locally
|
|
133
239
|
setProfile((prev) => ({
|
|
@@ -159,16 +265,17 @@ export function ProfilePage() {
|
|
|
159
265
|
headers: {
|
|
160
266
|
"Content-Type": "application/json",
|
|
161
267
|
},
|
|
268
|
+
credentials: "include",
|
|
162
269
|
body: JSON.stringify({
|
|
163
270
|
avatar_url: null,
|
|
164
271
|
}),
|
|
165
272
|
});
|
|
166
273
|
if (!response.ok) {
|
|
167
|
-
|
|
274
|
+
logger.error("Failed to update avatar_url in profile");
|
|
168
275
|
}
|
|
169
276
|
}
|
|
170
277
|
catch (error) {
|
|
171
|
-
|
|
278
|
+
logger.error("Error updating profile avatar_url:", error);
|
|
172
279
|
}
|
|
173
280
|
// Update profile locally
|
|
174
281
|
setProfile((prev) => ({ ...prev, avatar_url: "" }));
|
|
@@ -176,7 +283,7 @@ export function ProfilePage() {
|
|
|
176
283
|
if (isLoading) {
|
|
177
284
|
return (_jsx("div", { className: "flex justify-center items-center min-h-[400px]", children: _jsx(Spinner, { size: "lg", label: t("profile.loading") || "Loading profile..." }) }));
|
|
178
285
|
}
|
|
179
|
-
return (_jsx("div", { className: "pt-12 pb-12 max-w-
|
|
286
|
+
return (_jsx("div", { className: "md:pt-12 pb-12 max-w-2xl mx-auto md:px-4", children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs("div", { className: "space-y-6", children: [_jsx("div", { className: "flex justify-center", children: _jsx(AvatarUploader, { userId: currentUser?.id, bucket: "avatar", shape: "circle", onUpload: handleAvatarUpload, onDelete: handleAvatarDelete, initialAvatarPath: currentUser?.user_metadata?.avatar ||
|
|
180
287
|
profile.avatar_url ||
|
|
181
288
|
null, initialAvatarSizes: (() => {
|
|
182
289
|
const sizes = currentUser?.user_metadata
|
|
@@ -193,9 +300,21 @@ export function ProfilePage() {
|
|
|
193
300
|
}, onDeleted: () => {
|
|
194
301
|
setProfile((prev) => ({ ...prev, avatar_url: "" }));
|
|
195
302
|
} }) }), _jsxs("div", { className: "flex items-center gap-2 mb-4", children: [_jsx(User, { className: "w-8 h-8" }), _jsx("h1", { className: "text-3xl font-bold", children: t("profile.edit") || "Edit Profile" })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: t("profile.personal_info") || "Personal Information" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: t("profile.first_name") || "First Name", placeholder: t("profile.first_name_placeholder") ||
|
|
196
|
-
"Enter your first name", value: profile.first_name || "", onChange: (e) => handleChange("first_name", e.target.value) }), _jsx(Input, { label: t("profile.last_name") || "Last Name", placeholder: t("profile.last_name_placeholder") || "Enter your last name", value: profile.last_name || "", onChange: (e) => handleChange("last_name", e.target.value) }), _jsx(Input, { label: t("profile.phone") || "Phone", placeholder: t("profile.phone_placeholder") || "Enter your phone number", type: "tel", value: profile.phone || "", onChange: (e) => handleChange("phone", e.target.value), className: "md:col-span-2" }), _jsx(
|
|
197
|
-
|
|
198
|
-
|
|
303
|
+
"Enter your first name", value: profile.first_name || "", onChange: (e) => handleChange("first_name", e.target.value) }), _jsx(Input, { label: t("profile.last_name") || "Last Name", placeholder: t("profile.last_name_placeholder") || "Enter your last name", value: profile.last_name || "", onChange: (e) => handleChange("last_name", e.target.value) }), _jsx(Input, { label: t("profile.phone") || "Phone", placeholder: t("profile.phone_placeholder") || "Enter your phone number", type: "tel", value: profile.phone || "", onChange: (e) => handleChange("phone", e.target.value), className: "md:col-span-2" }), _jsx(Input, { label: "Username", placeholder: "Enter your username", value: profile.username || "", onChange: (e) => handleChange("username", e.target.value), description: "3-30 characters, alphanumeric, underscore or dash only", errorMessage: checkingUsername
|
|
304
|
+
? "Checking..."
|
|
305
|
+
: usernameAvailable === false
|
|
306
|
+
? "Username already taken"
|
|
307
|
+
: undefined, color: checkingUsername
|
|
308
|
+
? "default"
|
|
309
|
+
: usernameAvailable === true
|
|
310
|
+
? "success"
|
|
311
|
+
: usernameAvailable === false
|
|
312
|
+
? "danger"
|
|
313
|
+
: "default", className: "md:col-span-2", endContent: checkingUsername ? (_jsx(Spinner, { size: "sm" })) : profile.username && profile.username.length > 0 ? (usernameAvailable === false ? (_jsx(XCircle, { className: "w-4 h-4 text-danger" })) : (_jsx(Check, { className: "w-4 h-4 text-success" }))) : null }), _jsx(Textarea, { label: t("profile.bio") || "Bio", placeholder: t("profile.bio_placeholder") || "Tell us about yourself", value: profile.bio || "", onChange: (e) => handleChange("bio", e.target.value), minRows: 3, className: "md:col-span-2" })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: t("profile.security") || "Sécurité" }) }), _jsx(Divider, {}), _jsxs(CardBody, { className: "space-y-4", children: [_jsxs("div", { className: "grid gap-4 md:grid-cols-2 items-center justify-center", children: [_jsx(Input, { label: t("profile.security_new_email") || "Nouvel email", placeholder: t("profile.security_new_email_placeholder") ||
|
|
314
|
+
"nouveau@email.com", value: newEmail, onChange: (e) => setNewEmail(e.target.value) }), _jsx(Button, { onPress: handleEmailChangeRequest, color: "secondary", size: "lg", variant: "flat", isLoading: securityLoading, className: "w-full md:w-auto", children: t("profile.security_email_change_btn") ||
|
|
315
|
+
"Envoyer la confirmation" })] }), _jsx(Divider, { className: "my-2" }), _jsxs("div", { className: "flex flex-col md:flex-row md:items-center md:justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h4", { className: "font-medium", children: t("profile.security_reset_title") ||
|
|
316
|
+
"Réinitialiser le mot de passe" }), _jsx("p", { className: "text-sm text-default-500", children: t("profile.security_reset_help") ||
|
|
317
|
+
"Nous enverrons un lien de réinitialisation à votre email" })] }), _jsx(Button, { variant: "flat", color: "secondary", onPress: handleSendResetLink, isLoading: securityLoading, className: "w-full md:w-auto", children: t("profile.security_reset_btn") || "Envoyer le lien" })] })] })] }), _jsxs("div", { className: "flex justify-center gap-3", children: [_jsx(Button, { type: "button", variant: "flat", onPress: () => fetchProfile(), isDisabled: isSaving, children: t("profile.cancel_button") || "Cancel" }), _jsx(Button, { type: "submit", color: "primary", isLoading: isSaving, startContent: !isSaving && _jsx(Save, { className: "w-4 h-4" }), children: isSaving
|
|
199
318
|
? t("profile.saving") || "Saving..."
|
|
200
319
|
: t("profile.save_button") || "Save Changes" })] })] }) }) }));
|
|
201
320
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reglage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/reglage.tsx"],"names":[],"mappings":"AAiCA,wBAAgB,WAAW,
|
|
1
|
+
{"version":3,"file":"reglage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/reglage.tsx"],"names":[],"mappings":"AAiCA,wBAAgB,WAAW,4CAmT1B"}
|