@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
@@ -0,0 +1,141 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSupabaseServerClient } from "@lastbrain/core/server";
3
+ import { getStorageUsage, StorageUsageData } from "@lastbrain/core";
4
+
5
+ // Cache simple pour éviter de recalculer trop souvent
6
+ const storageCache = new Map<
7
+ string,
8
+ { data: StorageUsageData; timestamp: number }
9
+ >();
10
+ const CACHE_TTL = 30 * 1000; // 30 secondes
11
+
12
+ // Nettoyer les entrées expirées du cache
13
+ function cleanExpiredCache() {
14
+ const now = Date.now();
15
+ for (const [key, value] of storageCache.entries()) {
16
+ if (now - value.timestamp > CACHE_TTL) {
17
+ storageCache.delete(key);
18
+ }
19
+ }
20
+ }
21
+
22
+ /**
23
+ * GET /api/admin/storage/usage
24
+ * Admin route: superadmin can query any user's storage usage
25
+ * Query params:
26
+ * - ownerId: required, user ID to check
27
+ * - forceRefresh: optional, bypass cache
28
+ */
29
+ export async function GET(request: NextRequest) {
30
+ try {
31
+ const supabase = await getSupabaseServerClient();
32
+
33
+ // Vérifier l'authentification
34
+ const {
35
+ data: { user },
36
+ error: authError,
37
+ } = await supabase.auth.getUser();
38
+
39
+ if (authError || !user) {
40
+ return NextResponse.json(
41
+ { success: false, error: "Non authentifié" },
42
+ { status: 401 }
43
+ );
44
+ }
45
+
46
+ // Vérifier si l'utilisateur est superadmin
47
+ const { data: isSuperAdmin, error: adminCheckError } = await supabase.rpc(
48
+ "is_superadmin",
49
+ { user_id: user.id }
50
+ );
51
+
52
+ if (adminCheckError || !isSuperAdmin) {
53
+ return NextResponse.json(
54
+ {
55
+ success: false,
56
+ error: "Accès non autorisé - réservé aux superadmins",
57
+ },
58
+ { status: 403 }
59
+ );
60
+ }
61
+
62
+ // Récupérer l'ownerId depuis les paramètres (requis pour la route admin)
63
+ const { searchParams } = new URL(request.url);
64
+ const ownerId = searchParams.get("ownerId");
65
+ const forceRefresh = searchParams.get("forceRefresh") === "true";
66
+
67
+ if (!ownerId) {
68
+ return NextResponse.json(
69
+ { success: false, error: "ownerId est requis" },
70
+ { status: 400 }
71
+ );
72
+ }
73
+
74
+ // Nettoyer le cache avant utilisation
75
+ cleanExpiredCache();
76
+
77
+ // Vérifier si on a des données en cache (sauf si forceRefresh)
78
+ const cacheKey = `storage-${ownerId}`;
79
+ let shouldUseCache = false;
80
+
81
+ if (!forceRefresh) {
82
+ const cachedEntry = storageCache.get(cacheKey);
83
+ const now = Date.now();
84
+
85
+ if (cachedEntry && now - cachedEntry.timestamp < CACHE_TTL) {
86
+ // Vérifier d'abord si l'allocation de stockage a changé
87
+ const { data: userProfile } = await supabase
88
+ .from("user_profil")
89
+ .select("stockage")
90
+ .eq("owner_id", ownerId)
91
+ .single();
92
+
93
+ if (
94
+ userProfile &&
95
+ userProfile.stockage === cachedEntry.data.allocatedGb
96
+ ) {
97
+ shouldUseCache = true;
98
+ }
99
+ }
100
+ }
101
+
102
+ // Utiliser le cache si valide
103
+ if (shouldUseCache) {
104
+ const cached = storageCache.get(cacheKey);
105
+ if (cached) {
106
+ return NextResponse.json({
107
+ success: true,
108
+ data: cached.data,
109
+ fromCache: true,
110
+ });
111
+ }
112
+ }
113
+
114
+ // Calculer les données de stockage
115
+ const storageData = await getStorageUsage(supabase, ownerId);
116
+
117
+ // Mettre en cache
118
+ storageCache.set(cacheKey, {
119
+ data: storageData,
120
+ timestamp: Date.now(),
121
+ });
122
+
123
+ return NextResponse.json({
124
+ success: true,
125
+ data: storageData,
126
+ fromCache: false,
127
+ });
128
+ } catch (error) {
129
+ console.error("[Admin Storage Usage] Error:", error);
130
+ return NextResponse.json(
131
+ {
132
+ success: false,
133
+ error:
134
+ error instanceof Error
135
+ ? error.message
136
+ : "Erreur lors du calcul du stockage",
137
+ },
138
+ { status: 500 }
139
+ );
140
+ }
141
+ }
@@ -1,5 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { getSupabaseServerClient } from "@lastbrain/core/server";
3
+ import { logger } from "@lastbrain/core";
3
4
 
4
5
  /**
5
6
  * POST /api/admin/users/[id]/notifications
@@ -44,7 +45,7 @@ export async function POST(
44
45
  .single();
45
46
 
46
47
  if (error) {
47
- console.error("Error creating notification:", error);
48
+ logger.error("Error creating notification:", error);
48
49
  return NextResponse.json(
49
50
  { error: "Database Error", message: error.message },
50
51
  { status: 500 }
@@ -56,7 +57,7 @@ export async function POST(
56
57
  notification: data,
57
58
  });
58
59
  } catch (error) {
59
- console.error("Error in send notification endpoint:", error);
60
+ logger.error("Error in send notification endpoint:", error);
60
61
  return NextResponse.json(
61
62
  {
62
63
  error: "Internal Server Error",
@@ -1,5 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { getSupabaseServerClient } from "@lastbrain/core/server";
3
+ import { logger } from "@lastbrain/core";
3
4
 
4
5
  /**
5
6
  * GET /api/admin/users/[id]
@@ -27,7 +28,7 @@ export async function GET(
27
28
  );
28
29
 
29
30
  if (userError) {
30
- console.error("Error fetching user details:", userError);
31
+ logger.error("Error fetching user details:", userError);
31
32
  return NextResponse.json(
32
33
  { error: "Database Error", message: userError.message },
33
34
  { status: 500 }
@@ -40,7 +41,7 @@ export async function GET(
40
41
 
41
42
  return NextResponse.json(userDetails);
42
43
  } catch (error) {
43
- console.error("Error in admin user details endpoint:", error);
44
+ logger.error("Error in admin user details endpoint:", error);
44
45
  return NextResponse.json(
45
46
  {
46
47
  error: "Internal Server Error",
@@ -0,0 +1,88 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
3
+ import { logger } from "@lastbrain/core";
4
+
5
+ /**
6
+ * POST /api/admin/users/reactivate/[id]
7
+ * Attempt to reactivate a user by clearing the suspended flag in app metadata (service role required)
8
+ */
9
+ export async function POST(
10
+ request: NextRequest,
11
+ context: { params: Promise<{ id: string }> }
12
+ ) {
13
+ try {
14
+ const supabase = await getSupabaseServiceClient();
15
+ const { id: userId } = await context.params;
16
+
17
+ if (!userId) {
18
+ return NextResponse.json(
19
+ { error: "User ID is required" },
20
+ { status: 400 }
21
+ );
22
+ }
23
+
24
+ // Try using the Admin auth API if available
25
+ try {
26
+ // @ts-expect-error - supabase-js may expose auth.admin.updateUserById
27
+ if (supabase.auth?.admin?.updateUserById) {
28
+ // set app_metadata.suspended = false
29
+ const { data, error } = await (
30
+ supabase.auth as any
31
+ ).admin.updateUserById(userId, {
32
+ app_metadata: { suspended: false },
33
+ });
34
+
35
+ if (error) {
36
+ logger.error(
37
+ "Error reactivating user (admin.updateUserById):",
38
+ error
39
+ );
40
+ return NextResponse.json(
41
+ { error: "Database Error", message: error.message },
42
+ { status: 500 }
43
+ );
44
+ }
45
+
46
+ return NextResponse.json({ success: true, user: data });
47
+ }
48
+ } catch (err) {
49
+ logger.debug("auth.admin.updateUserById not available or failed:", err);
50
+ }
51
+
52
+ // Fallback: try to update auth.users via SQL (may require service role privileges)
53
+ try {
54
+ const update = {
55
+ raw_app_meta_data: { suspended: false },
56
+ };
57
+
58
+ const { data, error } = await supabase
59
+ .from("users")
60
+ .update(update)
61
+ .eq("id", userId)
62
+ .select()
63
+ .single();
64
+
65
+ if (error) {
66
+ logger.error("Fallback reactivate user failed:", error);
67
+ return NextResponse.json(
68
+ { error: "Database Error", message: error.message },
69
+ { status: 500 }
70
+ );
71
+ }
72
+
73
+ return NextResponse.json({ success: true, user: data });
74
+ } catch (err) {
75
+ logger.error("Final reactivate fallback error:", err);
76
+ return NextResponse.json(
77
+ { error: "Internal Server Error", message: String(err) },
78
+ { status: 500 }
79
+ );
80
+ }
81
+ } catch (error) {
82
+ logger.error("Error in reactivate user endpoint:", error);
83
+ return NextResponse.json(
84
+ { error: "Internal Server Error", message: "Failed to reactivate user" },
85
+ { status: 500 }
86
+ );
87
+ }
88
+ }
@@ -0,0 +1,85 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
3
+ import { logger } from "@lastbrain/core";
4
+
5
+ /**
6
+ * POST /api/admin/users/suspend/[id]
7
+ * Attempt to suspend a user by setting a suspended flag in app metadata (service role required)
8
+ */
9
+ export async function POST(
10
+ request: NextRequest,
11
+ context: { params: Promise<{ id: string }> }
12
+ ) {
13
+ try {
14
+ const supabase = await getSupabaseServiceClient();
15
+ const { id: userId } = await context.params;
16
+
17
+ if (!userId) {
18
+ return NextResponse.json(
19
+ { error: "User ID is required" },
20
+ { status: 400 }
21
+ );
22
+ }
23
+
24
+ // Try using the Admin auth API if available
25
+ try {
26
+ // @ts-expect-error - supabase-js may expose auth.admin.updateUserById
27
+ if (supabase.auth?.admin?.updateUserById) {
28
+ // set app_metadata.suspended = true
29
+ const { data, error } = await (
30
+ supabase.auth as any
31
+ ).admin.updateUserById(userId, {
32
+ app_metadata: { suspended: true },
33
+ });
34
+
35
+ if (error) {
36
+ logger.error("Error suspending user (admin.updateUserById):", error);
37
+ return NextResponse.json(
38
+ { error: "Database Error", message: error.message },
39
+ { status: 500 }
40
+ );
41
+ }
42
+
43
+ return NextResponse.json({ success: true, user: data });
44
+ }
45
+ } catch (err) {
46
+ logger.debug("auth.admin.updateUserById not available or failed:", err);
47
+ }
48
+
49
+ // Fallback: try to update auth.users via SQL (may require service role privileges)
50
+ try {
51
+ const update = {
52
+ raw_app_meta_data: { suspended: true },
53
+ };
54
+
55
+ const { data, error } = await supabase
56
+ .from("users")
57
+ .update(update)
58
+ .eq("id", userId)
59
+ .select()
60
+ .single();
61
+
62
+ if (error) {
63
+ logger.error("Fallback suspend user failed:", error);
64
+ return NextResponse.json(
65
+ { error: "Database Error", message: error.message },
66
+ { status: 500 }
67
+ );
68
+ }
69
+
70
+ return NextResponse.json({ success: true, user: data });
71
+ } catch (err) {
72
+ logger.error("Final suspend fallback error:", err);
73
+ return NextResponse.json(
74
+ { error: "Internal Server Error", message: String(err) },
75
+ { status: 500 }
76
+ );
77
+ }
78
+ } catch (error) {
79
+ logger.error("Error in suspend user endpoint:", error);
80
+ return NextResponse.json(
81
+ { error: "Internal Server Error", message: "Failed to suspend user" },
82
+ { status: 500 }
83
+ );
84
+ }
85
+ }
@@ -1,3 +1,4 @@
1
+ import { logger } from "@lastbrain/core";
1
2
  import { getSupabaseServiceClient } from "@lastbrain/core/server";
2
3
  import { NextRequest, NextResponse } from "next/server";
3
4
 
@@ -78,7 +79,7 @@ export async function GET(request: NextRequest) {
78
79
  { status: 200 }
79
80
  );
80
81
  } catch (error) {
81
- console.error("Error fetching users by signup source:", error);
82
+ logger.error("Error fetching users by signup source:", error);
82
83
  return NextResponse.json(
83
84
  { error: "Erreur interne du serveur" },
84
85
  { status: 500 }
@@ -1,5 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { getSupabaseServerClient } from "@lastbrain/core/server";
3
+ import { logger } from "@lastbrain/core";
3
4
 
4
5
  /**
5
6
  * GET /api/admin/users
@@ -29,7 +30,7 @@ export async function GET(request: NextRequest) {
29
30
  );
30
31
 
31
32
  if (usersError) {
32
- console.error("Error fetching users:", usersError);
33
+ logger.error("Error fetching users:", usersError);
33
34
  return NextResponse.json(
34
35
  { error: "Database Error", message: usersError.message },
35
36
  { status: 500 }
@@ -37,6 +38,62 @@ export async function GET(request: NextRequest) {
37
38
  }
38
39
 
39
40
  // The RPC function returns the complete response with data and pagination
41
+ // Ensure each user has a normalized `app_metadata` field derived from
42
+ // `raw_app_meta_data` (RPCs may differ in the exact shape returned).
43
+ if (result && Array.isArray((result as any).data)) {
44
+ try {
45
+ const mapped = (result as any).data.map((u: any) => {
46
+ const out = { ...u };
47
+
48
+ // Try multiple possible locations for app metadata returned by different RPCs
49
+ const rawCandidate =
50
+ out.raw_app_meta_data ??
51
+ out.metadata ??
52
+ out.raw_user_meta_data ??
53
+ out.app_metadata ??
54
+ null;
55
+
56
+ // rawCandidate can be an object or a JSON string
57
+ let parsedRaw: any = rawCandidate;
58
+ if (typeof rawCandidate === "string") {
59
+ try {
60
+ parsedRaw = JSON.parse(rawCandidate);
61
+ } catch (e) {
62
+ parsedRaw = rawCandidate;
63
+ }
64
+ }
65
+
66
+ // If parsedRaw contains an `app_metadata` object, prefer it.
67
+ // Otherwise, if parsedRaw itself looks like metadata (contains suspended or provider keys), use it.
68
+ let derived: any = null;
69
+ if (parsedRaw && typeof parsedRaw === "object") {
70
+ if (
71
+ parsedRaw.app_metadata &&
72
+ typeof parsedRaw.app_metadata === "object"
73
+ ) {
74
+ derived = parsedRaw.app_metadata;
75
+ } else {
76
+ // parsedRaw may already be the app metadata
77
+ derived = parsedRaw;
78
+ }
79
+ }
80
+
81
+ // Ensure out.app_metadata is an object when possible
82
+ if (derived && typeof derived === "object") {
83
+ out.app_metadata = { ...(out.app_metadata || {}), ...derived };
84
+ } else if (out.app_metadata == null) {
85
+ out.app_metadata = null;
86
+ }
87
+
88
+ return out;
89
+ });
90
+
91
+ (result as any).data = mapped;
92
+ } catch (e) {
93
+ logger.debug("Failed to normalize app_metadata for users list:", e);
94
+ }
95
+ }
96
+
40
97
  return NextResponse.json(
41
98
  result || {
42
99
  data: [],
@@ -44,7 +101,7 @@ export async function GET(request: NextRequest) {
44
101
  }
45
102
  );
46
103
  } catch (error) {
47
- console.error("Error in admin users endpoint:", error);
104
+ logger.error("Error in admin users endpoint:", error);
48
105
  return NextResponse.json(
49
106
  {
50
107
  error: "Internal Server Error",
@@ -0,0 +1,54 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSupabaseServerClient } from "@lastbrain/core/server";
3
+ import { sendEmailChangeConfirmation } from "../../../lib/auth-email-service";
4
+ import { logger } from "@lastbrain/core";
5
+
6
+ function extractLocale(request: NextRequest): string | undefined {
7
+ const lang = request.headers
8
+ .get("accept-language")
9
+ ?.split(",")?.[0]
10
+ ?.split("-")?.[0];
11
+ return lang === "fr" || lang === "en" ? lang : undefined;
12
+ }
13
+
14
+ export async function POST(request: NextRequest) {
15
+ try {
16
+ const locale = extractLocale(request);
17
+ const supabase = await getSupabaseServerClient();
18
+ const {
19
+ data: { user },
20
+ error: authError,
21
+ } = await supabase.auth.getUser();
22
+
23
+ if (authError || !user) {
24
+ return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
25
+ }
26
+
27
+ const body = await request.json();
28
+ const newEmail = body?.newEmail as string;
29
+
30
+ if (!newEmail) {
31
+ return NextResponse.json(
32
+ { error: "Nouvel email requis" },
33
+ { status: 400 }
34
+ );
35
+ }
36
+
37
+ await sendEmailChangeConfirmation({
38
+ email: user.email || "",
39
+ newEmail,
40
+ displayName: (user.user_metadata as any)?.full_name,
41
+ locale,
42
+ ownerId: user.id,
43
+ redirectTo: `${request.nextUrl.origin}/api/public/callback?next=/confirm`,
44
+ });
45
+
46
+ return NextResponse.json({ message: "Lien de confirmation envoyé" });
47
+ } catch (error) {
48
+ logger.error("email-change error:", error);
49
+ return NextResponse.json(
50
+ { error: "Impossible d'envoyer le lien de confirmation" },
51
+ { status: 500 }
52
+ );
53
+ }
54
+ }
@@ -0,0 +1,47 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSupabaseServerClient } from "@lastbrain/core/server";
3
+ import { sendPasswordResetEmail } from "../../../lib/auth-email-service";
4
+ import { logger } from "@lastbrain/core";
5
+ import { resolveSiteUrl } from "../../../lib/site-url";
6
+
7
+ function extractLocale(request: NextRequest): string | undefined {
8
+ const lang = request.headers
9
+ .get("accept-language")
10
+ ?.split(",")?.[0]
11
+ ?.split("-")?.[0];
12
+ return lang === "fr" || lang === "en" ? lang : undefined;
13
+ }
14
+
15
+ const DEFAULT_LOCALE = "fr";
16
+
17
+ export async function POST(request: NextRequest) {
18
+ try {
19
+ const locale = extractLocale(request);
20
+ const supabase = await getSupabaseServerClient();
21
+ const {
22
+ data: { user },
23
+ error,
24
+ } = await supabase.auth.getUser();
25
+
26
+ if (error || !user) {
27
+ return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
28
+ }
29
+
30
+ const resolvedLocale = locale || DEFAULT_LOCALE;
31
+ await sendPasswordResetEmail({
32
+ email: user.email || "",
33
+ displayName: (user.user_metadata as any)?.full_name,
34
+ ownerId: user.id,
35
+ redirectTo: `${resolveSiteUrl(request)}/${resolvedLocale}/reset-password`,
36
+ locale,
37
+ });
38
+
39
+ return NextResponse.json({ message: "Lien de réinitialisation envoyé" });
40
+ } catch (err) {
41
+ logger.error("account reset-password error:", err);
42
+ return NextResponse.json(
43
+ { error: "Impossible d'envoyer le lien de réinitialisation" },
44
+ { status: 500 }
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Check username availability
3
+ * GET /api/auth/check-username?username=xxx&owner_id=xxx
4
+ */
5
+
6
+ import { NextRequest, NextResponse } from "next/server";
7
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
8
+
9
+ export async function GET(req: NextRequest) {
10
+ try {
11
+ const { searchParams } = new URL(req.url);
12
+ const username = searchParams.get("username");
13
+ const owner_id = searchParams.get("owner_id");
14
+
15
+ if (!username) {
16
+ return NextResponse.json(
17
+ { available: false, error: "Username required" },
18
+ { status: 400 }
19
+ );
20
+ }
21
+
22
+ const supabase = getSupabaseServiceClient();
23
+
24
+ // Check if username exists (case-insensitive), excluding current user
25
+ let query = supabase
26
+ .from("user_profil")
27
+ .select("username")
28
+ .ilike("username", username)
29
+ .limit(1);
30
+
31
+ if (owner_id) {
32
+ query = query.neq("owner_id", owner_id);
33
+ }
34
+
35
+ const { data, error } = await query;
36
+
37
+ if (error) {
38
+ return NextResponse.json(
39
+ { available: false, error: error.message },
40
+ { status: 500 }
41
+ );
42
+ }
43
+
44
+ return NextResponse.json({ available: !data || data.length === 0 });
45
+ } catch (error) {
46
+ console.error("[check-username] Error:", error);
47
+ return NextResponse.json(
48
+ { available: false, error: "Internal server error" },
49
+ { status: 500 }
50
+ );
51
+ }
52
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Establish server-side session with cookies
3
+ * Called after client-side setSession() to ensure HTTP-only cookies are set
4
+ */
5
+ import { logger } from "@lastbrain/core";
6
+ import { getSupabaseServerClient } from "@lastbrain/core/server";
7
+
8
+ export async function POST(request: Request) {
9
+ try {
10
+ const supabase = await getSupabaseServerClient();
11
+
12
+ // Verify session exists on server
13
+ const {
14
+ data: { user },
15
+ error: userError,
16
+ } = await supabase.auth.getUser();
17
+
18
+ if (userError || !user) {
19
+ logger.error("❌ No user session on server:", userError);
20
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
21
+ }
22
+
23
+ // Cookies are automatically set by Supabase middleware
24
+ return Response.json({ success: true, user: user.id });
25
+ } catch (error) {
26
+ logger.debug("💥 Error establishing session:", error);
27
+ return Response.json(
28
+ { error: "Failed to establish session" },
29
+ { status: 500 }
30
+ );
31
+ }
32
+ }