@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.
Files changed (179) hide show
  1. package/README.md +55 -7
  2. package/dist/api/admin/signup-stats.d.ts.map +1 -1
  3. package/dist/api/admin/signup-stats.js +2 -1
  4. package/dist/api/admin/storage/usage.d.ts +18 -0
  5. package/dist/api/admin/storage/usage.d.ts.map +1 -0
  6. package/dist/api/admin/storage/usage.js +100 -0
  7. package/dist/api/admin/users/[id]/notifications.d.ts.map +1 -1
  8. package/dist/api/admin/users/[id]/notifications.js +3 -2
  9. package/dist/api/admin/users/[id].d.ts.map +1 -1
  10. package/dist/api/admin/users/[id].js +3 -2
  11. package/dist/api/admin/users/reactivate/[id].d.ts +16 -0
  12. package/dist/api/admin/users/reactivate/[id].d.ts.map +1 -0
  13. package/dist/api/admin/users/reactivate/[id].js +59 -0
  14. package/dist/api/admin/users/suspend/[id].d.ts +16 -0
  15. package/dist/api/admin/users/suspend/[id].d.ts.map +1 -0
  16. package/dist/api/admin/users/suspend/[id].js +59 -0
  17. package/dist/api/admin/users-by-source.d.ts.map +1 -1
  18. package/dist/api/admin/users-by-source.js +2 -1
  19. package/dist/api/admin/users.d.ts.map +1 -1
  20. package/dist/api/admin/users.js +53 -2
  21. package/dist/api/auth/account/email-change.d.ts +7 -0
  22. package/dist/api/auth/account/email-change.d.ts.map +1 -0
  23. package/dist/api/auth/account/email-change.js +39 -0
  24. package/dist/api/auth/account/reset-password.d.ts +7 -0
  25. package/dist/api/auth/account/reset-password.d.ts.map +1 -0
  26. package/dist/api/auth/account/reset-password.js +36 -0
  27. package/dist/api/auth/check-username.d.ts +9 -0
  28. package/dist/api/auth/check-username.d.ts.map +1 -0
  29. package/dist/api/auth/check-username.js +35 -0
  30. package/dist/api/auth/establish-session.d.ts +2 -0
  31. package/dist/api/auth/establish-session.d.ts.map +1 -0
  32. package/dist/api/auth/establish-session.js +23 -0
  33. package/dist/api/auth/me.d.ts +4 -4
  34. package/dist/api/auth/me.d.ts.map +1 -1
  35. package/dist/api/auth/me.js +28 -6
  36. package/dist/api/auth/profile.d.ts.map +1 -1
  37. package/dist/api/auth/profile.js +6 -3
  38. package/dist/api/auth/storage/recalculate.d.ts +15 -0
  39. package/dist/api/auth/storage/recalculate.d.ts.map +1 -0
  40. package/dist/api/auth/storage/recalculate.js +68 -0
  41. package/dist/api/auth/storage/usage.d.ts +10 -0
  42. package/dist/api/auth/storage/usage.d.ts.map +1 -0
  43. package/dist/api/auth/storage/usage.js +86 -0
  44. package/dist/api/public/add-welcome-bonus.d.ts +16 -0
  45. package/dist/api/public/add-welcome-bonus.d.ts.map +1 -0
  46. package/dist/api/public/add-welcome-bonus.js +177 -0
  47. package/dist/api/public/callback.d.ts +3 -0
  48. package/dist/api/public/callback.d.ts.map +1 -0
  49. package/dist/api/public/callback.js +197 -0
  50. package/dist/api/public/reset-password.d.ts +3 -0
  51. package/dist/api/public/reset-password.d.ts.map +1 -0
  52. package/dist/api/public/reset-password.js +43 -0
  53. package/dist/api/public/set-session.d.ts +7 -0
  54. package/dist/api/public/set-session.d.ts.map +1 -0
  55. package/dist/api/public/set-session.js +55 -0
  56. package/dist/api/public/signin.d.ts.map +1 -1
  57. package/dist/api/public/signin.js +31 -0
  58. package/dist/api/public/signup.d.ts.map +1 -1
  59. package/dist/api/public/signup.js +38 -27
  60. package/dist/api/public/webhook/storage-addon.d.ts +9 -0
  61. package/dist/api/public/webhook/storage-addon.d.ts.map +1 -0
  62. package/dist/api/public/webhook/storage-addon.js +155 -0
  63. package/dist/api/storage.js +2 -2
  64. package/dist/auth.build.config.d.ts.map +1 -1
  65. package/dist/auth.build.config.js +126 -11
  66. package/dist/components/AccountButton.d.ts.map +1 -1
  67. package/dist/components/AccountButton.js +54 -28
  68. package/dist/components/Doc.d.ts.map +1 -1
  69. package/dist/components/Doc.js +1 -1
  70. package/dist/components/HasProfil.d.ts +4 -0
  71. package/dist/components/HasProfil.d.ts.map +1 -0
  72. package/dist/components/HasProfil.js +39 -0
  73. package/dist/components/auth/dashboard.d.ts +1 -1
  74. package/dist/components/auth/dashboard.d.ts.map +1 -1
  75. package/dist/components/auth/dashboard.js +34 -7
  76. package/dist/index.d.ts +2 -0
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +2 -0
  79. package/dist/lib/app-branding-data.d.ts +22 -0
  80. package/dist/lib/app-branding-data.d.ts.map +1 -0
  81. package/dist/lib/app-branding-data.js +49 -0
  82. package/dist/lib/auth-email-service.d.ts +57 -0
  83. package/dist/lib/auth-email-service.d.ts.map +1 -0
  84. package/dist/lib/auth-email-service.js +382 -0
  85. package/dist/lib/auth-email-templates.d.ts +2 -0
  86. package/dist/lib/auth-email-templates.d.ts.map +1 -0
  87. package/dist/lib/auth-email-templates.js +1 -0
  88. package/dist/lib/site-url.d.ts +3 -0
  89. package/dist/lib/site-url.d.ts.map +1 -0
  90. package/dist/lib/site-url.js +11 -0
  91. package/dist/sitemap/manifest.d.ts +9 -0
  92. package/dist/sitemap/manifest.d.ts.map +1 -0
  93. package/dist/sitemap/manifest.js +14 -0
  94. package/dist/web/admin/signup-stats.js +3 -3
  95. package/dist/web/admin/user-detail.d.ts.map +1 -1
  96. package/dist/web/admin/user-detail.js +135 -14
  97. package/dist/web/admin/users-by-signup-source.js +2 -2
  98. package/dist/web/admin/users.d.ts.map +1 -1
  99. package/dist/web/admin/users.js +26 -7
  100. package/dist/web/auth/folder.d.ts.map +1 -1
  101. package/dist/web/auth/folder.js +4 -3
  102. package/dist/web/auth/profile.d.ts.map +1 -1
  103. package/dist/web/auth/profile.js +132 -13
  104. package/dist/web/auth/reglage.d.ts.map +1 -1
  105. package/dist/web/auth/reglage.js +15 -8
  106. package/dist/web/public/ResetPassword.d.ts.map +1 -1
  107. package/dist/web/public/ResetPassword.js +172 -2
  108. package/dist/web/public/SignInPage.d.ts.map +1 -1
  109. package/dist/web/public/SignInPage.js +39 -3
  110. package/dist/web/public/SignUpPage.d.ts.map +1 -1
  111. package/dist/web/public/SignUpPage.js +7 -2
  112. package/dist/web/public/auth-code-error.d.ts +2 -0
  113. package/dist/web/public/auth-code-error.d.ts.map +1 -0
  114. package/dist/web/public/auth-code-error.js +14 -0
  115. package/dist/web/public/confirm.d.ts +2 -0
  116. package/dist/web/public/confirm.d.ts.map +1 -0
  117. package/dist/web/public/confirm.js +157 -0
  118. package/package.json +10 -5
  119. package/src/api/admin/signup-stats.ts +2 -1
  120. package/src/api/admin/storage/usage.ts +141 -0
  121. package/src/api/admin/users/[id]/notifications.ts +3 -2
  122. package/src/api/admin/users/[id].ts +3 -2
  123. package/src/api/admin/users/reactivate/[id].ts +88 -0
  124. package/src/api/admin/users/suspend/[id].ts +85 -0
  125. package/src/api/admin/users-by-source.ts +2 -1
  126. package/src/api/admin/users.ts +59 -2
  127. package/src/api/auth/account/email-change.ts +54 -0
  128. package/src/api/auth/account/reset-password.ts +47 -0
  129. package/src/api/auth/check-username.ts +52 -0
  130. package/src/api/auth/establish-session.ts +32 -0
  131. package/src/api/auth/me.ts +29 -7
  132. package/src/api/auth/profile.ts +6 -2
  133. package/src/api/auth/storage/recalculate.ts +108 -0
  134. package/src/api/auth/storage/usage.ts +113 -0
  135. package/src/api/public/add-welcome-bonus.ts +229 -0
  136. package/src/api/public/callback.ts +307 -0
  137. package/src/api/public/reset-password.ts +52 -0
  138. package/src/api/public/set-session.ts +73 -0
  139. package/src/api/public/signin.ts +36 -0
  140. package/src/api/public/signup.ts +44 -37
  141. package/src/api/public/webhook/storage-addon.ts +267 -0
  142. package/src/api/storage.ts +2 -2
  143. package/src/auth.build.config.ts +126 -11
  144. package/src/components/AccountButton.tsx +114 -90
  145. package/src/components/Doc.tsx +47 -9
  146. package/src/components/HasProfil.tsx +63 -0
  147. package/src/components/auth/dashboard.tsx +54 -13
  148. package/src/i18n/en.json +76 -8
  149. package/src/i18n/es.json +330 -0
  150. package/src/i18n/fr.json +74 -8
  151. package/src/index.ts +2 -0
  152. package/src/lib/app-branding-data.ts +90 -0
  153. package/src/lib/auth-email-service.ts +508 -0
  154. package/src/lib/auth-email-templates.ts +5 -0
  155. package/src/lib/site-url.ts +17 -0
  156. package/src/sitemap/manifest.ts +26 -0
  157. package/src/web/admin/signup-stats.tsx +3 -3
  158. package/src/web/admin/user-detail.tsx +314 -15
  159. package/src/web/admin/users-by-signup-source.tsx +2 -2
  160. package/src/web/admin/users.tsx +50 -14
  161. package/src/web/auth/folder.tsx +23 -5
  162. package/src/web/auth/profile.tsx +227 -13
  163. package/src/web/auth/reglage.tsx +55 -24
  164. package/src/web/public/ResetPassword.tsx +301 -1
  165. package/src/web/public/SignInPage.tsx +43 -3
  166. package/src/web/public/SignUpPage.tsx +14 -5
  167. package/src/web/public/auth-code-error.tsx +49 -0
  168. package/src/web/public/confirm.tsx +195 -0
  169. package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +3 -1
  170. package/supabase/migrations/20251127100000_rename_body_to_message.sql +8 -3
  171. package/supabase/migrations/20260120150001_add_user_storage.sql +105 -0
  172. package/supabase/migrations/20260122131200_add_global_addons_system.sql +305 -0
  173. package/supabase/migrations/20260123100000_enable_vector_extension.sql +9 -0
  174. package/supabase/migrations/20260123140000_module_auth_fix_get_user_limits_null.sql +93 -0
  175. package/supabase/migrations/20260123145000_module_auth_fix_circular_storage_base.sql +89 -0
  176. package/supabase/migrations/20260129400000_add_username_to_user_profil.sql +33 -0
  177. package/dist/web/auth/dashboard.d.ts +0 -2
  178. package/dist/web/auth/dashboard.d.ts.map +0 -1
  179. 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
- console.error("Utilisateur non trouvé");
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
- console.error(t("user_detail.profile_loading_error") ||
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
- }, [fetchUserProfile]);
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
- return (_jsxs("div", { className: "max-w-[calc(100vw-8rem)] mx-auto 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
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: "danger", variant: "bordered", size: "sm", children: t("user_detail.suspend_account_btn") ||
152
- "Suspendre le compte" }), _jsx(Button, { color: "secondary", variant: "bordered", size: "sm", children: t("user_detail.promote_admin_btn") ||
153
- "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: t("user_detail.technical_metadata") ||
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"), moduleUserTabs.map((tab) => {
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":"AA8CA,wBAAgB,cAAc,4CAyQ7B"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"AAkDA,wBAAgB,cAAc,4CAyS7B"}
@@ -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
- console.error("Erreur lors du chargement des utilisateurs:", err);
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("span", { className: "text-small", children: user.last_sign_in_at
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":"AAqBA,wBAAgB,UAAU,mDA2EzB"}
1
+ {"version":3,"file":"folder.d.ts","sourceRoot":"","sources":["../../../src/web/auth/folder.tsx"],"names":[],"mappings":"AA2BA,wBAAgB,UAAU,mDAuFzB"}
@@ -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 mb-8", children: t("folder.title") || "Dossier" }), _jsx(FileManager, { bucket: "app", basePath: userData.id, allowUpload: true, allowCreateFolder: true, allowAIImageGeneration: false, className: "min-h-[80vh]" })] }));
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":"AA0CA,wBAAgB,WAAW,4CA8Z1B"}
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/web/auth/profile.tsx"],"names":[],"mappings":"AA2CA,wBAAgB,WAAW,4CAmnB1B"}
@@ -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
- console.error("Error fetching current user:", err);
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
- console.error("Error updating profile:", err);
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
- console.error("Failed to update avatar_url in profile");
232
+ logger.error("Failed to update avatar_url in profile");
127
233
  }
128
234
  }
129
235
  catch (error) {
130
- console.error("Error updating profile avatar_url:", error);
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
- console.error("Failed to update avatar_url in profile");
274
+ logger.error("Failed to update avatar_url in profile");
168
275
  }
169
276
  }
170
277
  catch (error) {
171
- console.error("Error updating profile avatar_url:", error);
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-4xl mx-auto 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 ||
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(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.professional_info") || "Professional Information" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: t("profile.company") || "Company", placeholder: t("profile.company_placeholder") ||
197
- "Enter your company name", value: profile.company || "", onChange: (e) => handleChange("company", e.target.value) }), _jsx(Input, { label: t("profile.website") || "Website", placeholder: t("profile.website_placeholder") || "https://example.com", type: "url", value: profile.website || "", onChange: (e) => handleChange("website", e.target.value) }), _jsx(Input, { label: t("profile.location") || "Location", placeholder: t("profile.location_placeholder") || "City, Country", value: profile.location || "", onChange: (e) => handleChange("location", e.target.value), className: "md:col-span-2" })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: t("profile.preferences") || "Preferences" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: t("profile.language") || "Language", placeholder: t("profile.language_placeholder") || "en, fr, es...", value: profile.language || "", onChange: (e) => handleChange("language", e.target.value) }), _jsx(Input, { label: t("profile.timezone") || "Timezone", placeholder: t("profile.timezone_placeholder") ||
198
- "Europe/Paris, America/New_York...", value: profile.timezone || "", onChange: (e) => handleChange("timezone", e.target.value) })] }) })] }), _jsxs("div", { className: "flex justify-end 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
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,4CAoR1B"}
1
+ {"version":3,"file":"reglage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/reglage.tsx"],"names":[],"mappings":"AAiCA,wBAAgB,WAAW,4CAmT1B"}