@lastbrain/module-auth 0.1.3 → 0.1.5

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 (44) hide show
  1. package/README.md +38 -9
  2. package/dist/api/admin/users.d.ts +1 -28
  3. package/dist/api/admin/users.d.ts.map +1 -1
  4. package/dist/api/admin/users.js +14 -63
  5. package/dist/api/auth/me.d.ts +3 -3
  6. package/dist/api/auth/me.d.ts.map +1 -1
  7. package/dist/api/auth/me.js +3 -5
  8. package/dist/api/auth/profile.d.ts.map +1 -1
  9. package/dist/api/auth/profile.js +4 -8
  10. package/dist/api/public/signin.js +3 -3
  11. package/dist/api/storage.d.ts.map +1 -1
  12. package/dist/api/storage.js +1 -1
  13. package/dist/auth.build.config.d.ts.map +1 -1
  14. package/dist/auth.build.config.js +21 -2
  15. package/dist/web/admin/users.d.ts.map +1 -1
  16. package/dist/web/admin/users.js +16 -9
  17. package/dist/web/auth/dashboard.d.ts.map +1 -1
  18. package/dist/web/auth/dashboard.js +2 -2
  19. package/dist/web/auth/profile.d.ts.map +1 -1
  20. package/dist/web/auth/profile.js +44 -4
  21. package/dist/web/auth/reglage.d.ts.map +1 -1
  22. package/dist/web/public/SignInPage.d.ts.map +1 -1
  23. package/dist/web/public/SignInPage.js +1 -1
  24. package/dist/web/public/SignUpPage.js +1 -1
  25. package/package.json +3 -2
  26. package/src/api/admin/users.ts +21 -89
  27. package/src/api/auth/me.ts +7 -14
  28. package/src/api/auth/profile.ts +10 -24
  29. package/src/api/public/signin.ts +3 -3
  30. package/src/api/storage.ts +8 -5
  31. package/src/auth.build.config.ts +21 -2
  32. package/src/web/admin/users.tsx +37 -11
  33. package/src/web/auth/dashboard.tsx +15 -8
  34. package/src/web/auth/profile.tsx +49 -7
  35. package/src/web/auth/reglage.tsx +17 -35
  36. package/src/web/public/SignInPage.tsx +1 -2
  37. package/src/web/public/SignUpPage.tsx +2 -2
  38. package/supabase/.temp/cli-latest +1 -0
  39. package/supabase/migrations/20251112000000_user_init.sql +1 -1
  40. package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +206 -0
  41. package/supabase/migrations/20251112000002_sync_avatars.sql +54 -0
  42. package/supabase/migrations-down/20251112000000_user_init.down.sql +2 -0
  43. package/supabase/migrations-down/20251112000001_auto_profile_and_admin_view.down.sql +23 -0
  44. package/supabase/migrations-down/20251112000002_sync_avatars.down.sql +9 -0
package/README.md CHANGED
@@ -25,6 +25,7 @@ pnpm add @lastbrain/module-auth
25
25
  ### Prérequis
26
26
 
27
27
  1. **Supabase configuré** : Vous devez avoir un projet Supabase avec les variables d'environnement suivantes :
28
+
28
29
  ```env
29
30
  NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
30
31
  NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
@@ -65,15 +66,18 @@ Le module fournit plusieurs composants React prêts à l'emploi utilisant HeroUI
65
66
  ### Pages Utilisateur Authentifié
66
67
 
67
68
  #### 1. Dashboard (`/auth/dashboard`)
69
+
68
70
  **Component**: `DashboardPage`
69
71
 
70
72
  Tableau de bord utilisateur affichant :
73
+
71
74
  - Résumé du profil avec avatar
72
75
  - Email et date de création du compte
73
76
  - Statut du compte
74
77
  - Statistiques rapides (projets, tâches, etc.)
75
78
 
76
79
  **Usage** :
80
+
77
81
  ```tsx
78
82
  import { DashboardPage } from "@lastbrain/module-auth";
79
83
 
@@ -83,14 +87,17 @@ export default function Dashboard() {
83
87
  ```
84
88
 
85
89
  #### 2. Profil (`/auth/profile`)
90
+
86
91
  **Component**: `ProfilePage`
87
92
 
88
93
  Page de modification du profil utilisateur permettant de mettre à jour :
94
+
89
95
  - Informations personnelles (nom, prénom, téléphone, bio)
90
96
  - Informations professionnelles (entreprise, site web, localisation)
91
97
  - Préférences (langue, timezone, avatar)
92
98
 
93
99
  **Usage** :
100
+
94
101
  ```tsx
95
102
  import { ProfilePage } from "@lastbrain/module-auth";
96
103
 
@@ -100,14 +107,17 @@ export default function Profile() {
100
107
  ```
101
108
 
102
109
  #### 3. Paramètres (`/auth/settings`)
110
+
103
111
  **Component**: `ReglagePage`
104
112
 
105
113
  Page de configuration du compte avec :
114
+
106
115
  - Notifications (email, push, marketing)
107
116
  - Apparence (thème)
108
117
  - Langue et région (langue, timezone)
109
118
 
110
119
  **Usage** :
120
+
111
121
  ```tsx
112
122
  import { ReglagePage } from "@lastbrain/module-auth";
113
123
 
@@ -119,11 +129,13 @@ export default function Settings() {
119
129
  ### Pages Publiques
120
130
 
121
131
  #### 4. Connexion (`/signin`)
132
+
122
133
  **Component**: `SignInPage`
123
134
 
124
135
  Page de connexion avec gestion des erreurs et redirection.
125
136
 
126
137
  **Usage** :
138
+
127
139
  ```tsx
128
140
  import { SignInPage } from "@lastbrain/module-auth";
129
141
 
@@ -133,11 +145,13 @@ export default function SignIn() {
133
145
  ```
134
146
 
135
147
  #### 5. Inscription (`/signup`)
148
+
136
149
  **Component**: `SignUpPage`
137
150
 
138
151
  Page d'inscription avec validation des champs.
139
152
 
140
153
  **Usage** :
154
+
141
155
  ```tsx
142
156
  import { SignUpPage } from "@lastbrain/module-auth";
143
157
 
@@ -149,18 +163,21 @@ export default function SignUp() {
149
163
  ### Pages Administration
150
164
 
151
165
  #### 6. Gestion des Utilisateurs (`/admin/users`)
166
+
152
167
  **Component**: `AdminUsersPage`
153
168
 
154
169
  Page d'administration pour lister et gérer tous les utilisateurs.
155
170
  **Accès réservé aux super admins uniquement.**
156
171
 
157
172
  Fonctionnalités :
173
+
158
174
  - Liste de tous les utilisateurs
159
175
  - Recherche par email ou nom
160
176
  - Pagination
161
177
  - Affichage des informations (email, rôle, date de création)
162
178
 
163
179
  **Usage** :
180
+
164
181
  ```tsx
165
182
  import { AdminUsersPage } from "@lastbrain/module-auth";
166
183
 
@@ -176,9 +193,11 @@ Le module expose plusieurs routes API pour la gestion des utilisateurs et profil
176
193
  ### API Authentification (`/api/auth/*`)
177
194
 
178
195
  #### GET `/api/auth/me`
196
+
179
197
  Récupère les informations de l'utilisateur connecté et son profil.
180
198
 
181
199
  **Réponse** :
200
+
182
201
  ```json
183
202
  {
184
203
  "data": {
@@ -203,12 +222,15 @@ Récupère les informations de l'utilisateur connecté et son profil.
203
222
  ```
204
223
 
205
224
  **Erreurs** :
225
+
206
226
  - `401` : Utilisateur non authentifié
207
227
 
208
228
  #### GET `/api/auth/profile`
229
+
209
230
  Récupère le profil de l'utilisateur connecté.
210
231
 
211
232
  **Réponse** :
233
+
212
234
  ```json
213
235
  {
214
236
  "data": {
@@ -222,9 +244,11 @@ Récupère le profil de l'utilisateur connecté.
222
244
  ```
223
245
 
224
246
  #### PUT/PATCH `/api/auth/profile`
247
+
225
248
  Met à jour le profil de l'utilisateur. Crée le profil s'il n'existe pas.
226
249
 
227
250
  **Body** :
251
+
228
252
  ```json
229
253
  {
230
254
  "first_name": "John",
@@ -245,6 +269,7 @@ Met à jour le profil de l'utilisateur. Crée le profil s'il n'existe pas.
245
269
  ```
246
270
 
247
271
  **Réponse** :
272
+
248
273
  ```json
249
274
  {
250
275
  "data": {
@@ -261,14 +286,17 @@ Met à jour le profil de l'utilisateur. Crée le profil s'il n'existe pas.
261
286
  ⚠️ **Toutes les routes admin nécessitent un accès super admin.**
262
287
 
263
288
  #### GET `/api/admin/users`
289
+
264
290
  Liste tous les utilisateurs (paginé).
265
291
 
266
292
  **Query Parameters** :
293
+
267
294
  - `page` (optionnel, défaut: 1) : Numéro de page
268
295
  - `per_page` (optionnel, défaut: 20) : Nombre d'éléments par page
269
296
  - `search` (optionnel) : Recherche par email ou nom
270
297
 
271
298
  **Réponse** :
299
+
272
300
  ```json
273
301
  {
274
302
  "data": [
@@ -293,6 +321,7 @@ Liste tous les utilisateurs (paginé).
293
321
  ```
294
322
 
295
323
  **Erreurs** :
324
+
296
325
  - `401` : Utilisateur non authentifié
297
326
  - `403` : Accès refusé (non super admin)
298
327
 
@@ -338,9 +367,11 @@ CREATE TABLE public.user_profil (
338
367
  ```
339
368
 
340
369
  **Index** :
370
+
341
371
  - `idx_user_profil_owner_id` sur `owner_id`
342
372
 
343
373
  **RLS (Row Level Security)** :
374
+
344
375
  - Les utilisateurs peuvent lire, créer, modifier et supprimer leur propre profil
345
376
  - Les super admins ont accès à tous les profils
346
377
 
@@ -446,6 +477,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
446
477
  ### Row Level Security (RLS)
447
478
 
448
479
  Toutes les tables sont protégées par RLS :
480
+
449
481
  - Les utilisateurs ne peuvent accéder qu'à leurs propres données
450
482
  - Les super admins ont accès à toutes les données
451
483
 
@@ -459,10 +491,7 @@ const { data: isSuperAdmin } = await supabase.rpc("is_superadmin", {
459
491
  });
460
492
 
461
493
  if (!isSuperAdmin) {
462
- return NextResponse.json(
463
- { error: "Forbidden" },
464
- { status: 403 }
465
- );
494
+ return NextResponse.json({ error: "Forbidden" }, { status: 403 });
466
495
  }
467
496
  ```
468
497
 
@@ -471,13 +500,13 @@ if (!isSuperAdmin) {
471
500
  Les routes API vérifient l'authentification :
472
501
 
473
502
  ```typescript
474
- const { data: { user }, error } = await supabase.auth.getUser();
503
+ const {
504
+ data: { user },
505
+ error,
506
+ } = await supabase.auth.getUser();
475
507
 
476
508
  if (error || !user) {
477
- return NextResponse.json(
478
- { error: "Unauthorized" },
479
- { status: 401 }
480
- );
509
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
481
510
  }
482
511
  ```
483
512
 
@@ -5,32 +5,5 @@ import { NextRequest, NextResponse } from "next/server";
5
5
  * Supports pagination via query params: page, per_page
6
6
  * Supports search via query param: search (email)
7
7
  */
8
- export declare function GET(request: NextRequest): Promise<NextResponse<{
9
- error: string;
10
- message: string;
11
- }> | NextResponse<{
12
- data: {
13
- id: any;
14
- email: string;
15
- created_at: any;
16
- profile: {
17
- first_name: any;
18
- last_name: any;
19
- avatar_url: any;
20
- bio: any;
21
- phone: any;
22
- company: any;
23
- website: any;
24
- location: any;
25
- language: any;
26
- timezone: any;
27
- };
28
- }[];
29
- pagination: {
30
- page: number;
31
- per_page: number;
32
- total: number;
33
- total_pages: number;
34
- };
35
- }>>;
8
+ export declare function GET(request: NextRequest): Promise<NextResponse<any>>;
36
9
  //# sourceMappingURL=users.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/api/admin/users.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;IAkH7C"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/api/admin/users.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW,8BA8C7C"}
@@ -9,75 +9,26 @@ import { getSupabaseServerClient } from "@lastbrain/core/server";
9
9
  export async function GET(request) {
10
10
  try {
11
11
  const supabase = await getSupabaseServerClient();
12
- // Check authentication
13
- const { data: { user }, error: authError, } = await supabase.auth.getUser();
14
- if (authError || !user) {
15
- return NextResponse.json({ error: "Unauthorized", message: "User not authenticated" }, { status: 401 });
16
- }
17
- // Check if user is superadmin
18
- const { data: isSuperAdmin } = await supabase.rpc("is_superadmin", {
19
- user_id: user.id,
20
- });
21
- if (!isSuperAdmin) {
22
- return NextResponse.json({ error: "Forbidden", message: "Superadmin access required" }, { status: 403 });
23
- }
12
+ // L'authentification et les droits superadmin sont déjà vérifiés par le middleware
24
13
  // Get query parameters
25
14
  const searchParams = request.nextUrl.searchParams;
26
15
  const page = parseInt(searchParams.get("page") || "1");
27
16
  const perPage = parseInt(searchParams.get("per_page") || "20");
28
17
  const search = searchParams.get("search") || "";
29
- // Note: We can only get public user data from auth.users via RPC or service role
30
- // For now, we'll query user_profil which has owner_id references
31
- let query = supabase
32
- .from("user_profil")
33
- .select("*, owner_id", { count: "exact" })
34
- .order("created_at", { ascending: false });
35
- // Add search filter if provided - using Supabase's built-in parameterized query
36
- if (search) {
37
- // Using .ilike with % wildcards - Supabase handles escaping
38
- query = query.or(`first_name.ilike.%${search}%,last_name.ilike.%${search}%`);
39
- }
40
- // Apply pagination
41
- const start = (page - 1) * perPage;
42
- query = query.range(start, start + perPage - 1);
43
- const { data: profiles, error: profileError, count } = await query;
44
- if (profileError) {
45
- console.error("Error fetching users:", profileError);
46
- return NextResponse.json({ error: "Database Error", message: profileError.message }, { status: 500 });
18
+ // Use RPC function to get users with emails
19
+ const { data: result, error: usersError } = await supabase.rpc("get_admin_users", {
20
+ page_number: page,
21
+ page_size: perPage,
22
+ search_term: search,
23
+ });
24
+ if (usersError) {
25
+ console.error("Error fetching users:", usersError);
26
+ return NextResponse.json({ error: "Database Error", message: usersError.message }, { status: 500 });
47
27
  }
48
- // Get auth users data in batch - more efficient than individual calls
49
- // Note: auth.admin methods require service role, so results may be limited
50
- // In production, consider creating a database view or RPC function to join
51
- // user_profil with auth.users for better performance
52
- const users = await Promise.all((profiles || []).map(async (profile) => {
53
- // Get basic auth info - this is limited to what's available
54
- const { data: authData } = await supabase.auth.admin.getUserById(profile.owner_id);
55
- return {
56
- id: profile.owner_id,
57
- email: authData?.user?.email || "N/A",
58
- created_at: authData?.user?.created_at || profile.created_at,
59
- profile: {
60
- first_name: profile.first_name,
61
- last_name: profile.last_name,
62
- avatar_url: profile.avatar_url,
63
- bio: profile.bio,
64
- phone: profile.phone,
65
- company: profile.company,
66
- website: profile.website,
67
- location: profile.location,
68
- language: profile.language,
69
- timezone: profile.timezone,
70
- },
71
- };
72
- }));
73
- return NextResponse.json({
74
- data: users,
75
- pagination: {
76
- page,
77
- per_page: perPage,
78
- total: count || 0,
79
- total_pages: Math.ceil((count || 0) / perPage),
80
- },
28
+ // The RPC function returns the complete response with data and pagination
29
+ return NextResponse.json(result || {
30
+ data: [],
31
+ pagination: { page, per_page: perPage, total: 0, total_pages: 0 },
81
32
  });
82
33
  }
83
34
  catch (error) {
@@ -4,14 +4,14 @@ import { NextResponse } from "next/server";
4
4
  * Returns the current authenticated user and their profile
5
5
  */
6
6
  export declare function GET(): Promise<NextResponse<{
7
- error: string;
8
- message: string;
9
- }> | NextResponse<{
10
7
  data: {
11
8
  id: string;
12
9
  email: string | undefined;
13
10
  created_at: string;
14
11
  profile: any;
15
12
  };
13
+ }> | NextResponse<{
14
+ error: string;
15
+ message: string;
16
16
  }>>;
17
17
  //# sourceMappingURL=me.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../src/api/auth/me.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C;;;GAGG;AACH,wBAAsB,GAAG;;;;;;;;;;IAwCxB"}
1
+ {"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../src/api/auth/me.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C;;;GAGG;AACH,wBAAsB,GAAG;;;;;;;;;;IAiCxB"}
@@ -8,12 +8,10 @@ export async function GET() {
8
8
  try {
9
9
  const supabase = await getSupabaseServerClient();
10
10
  // Get the authenticated user
11
- const { data: { user }, error: authError, } = await supabase.auth.getUser();
12
- if (authError || !user) {
13
- return NextResponse.json({ error: "Unauthorized", message: "User not authenticated" }, { status: 401 });
14
- }
11
+ const { data: { user }, } = await supabase.auth.getUser();
12
+ // L'utilisateur est déjà authentifié grâce au middleware
15
13
  // Get user profile
16
- const { data: profile, error: profileError } = await supabase
14
+ const { data: profile } = await supabase
17
15
  .from("user_profil")
18
16
  .select("*")
19
17
  .eq("owner_id", user.id)
@@ -1 +1 @@
1
- {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/api/auth/profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD;;;GAGG;AACH,wBAAsB,GAAG;;;;;IAsCxB;AAED;;;GAGG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;;IAgG7C;AAED;;;GAGG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,WAAW;;;;;IAE/C"}
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/api/auth/profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD;;;GAGG;AACH,wBAAsB,GAAG;;;;;IA+BxB;AAED;;;GAGG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;;IAyF7C;AAED;;;GAGG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,WAAW;;;;;IAE/C"}
@@ -7,10 +7,8 @@ import { getSupabaseServerClient } from "@lastbrain/core/server";
7
7
  export async function GET() {
8
8
  try {
9
9
  const supabase = await getSupabaseServerClient();
10
- const { data: { user }, error: authError, } = await supabase.auth.getUser();
11
- if (authError || !user) {
12
- return NextResponse.json({ error: "Unauthorized", message: "User not authenticated" }, { status: 401 });
13
- }
10
+ const { data: { user }, } = await supabase.auth.getUser();
11
+ // L'utilisateur est déjà authentifié grâce au middleware
14
12
  const { data: profile, error: profileError } = await supabase
15
13
  .from("user_profil")
16
14
  .select("*")
@@ -34,10 +32,8 @@ export async function GET() {
34
32
  export async function PUT(request) {
35
33
  try {
36
34
  const supabase = await getSupabaseServerClient();
37
- const { data: { user }, error: authError, } = await supabase.auth.getUser();
38
- if (authError || !user) {
39
- return NextResponse.json({ error: "Unauthorized", message: "User not authenticated" }, { status: 401 });
40
- }
35
+ const { data: { user }, } = await supabase.auth.getUser();
36
+ // L'utilisateur est déjà authentifié grâce au middleware
41
37
  const body = await request.json();
42
38
  const { first_name, last_name, avatar_url, bio, phone, company, website, location, language, timezone, preferences, } = body;
43
39
  // Check if profile exists
@@ -2,9 +2,9 @@ import { getSupabaseServerClient } from "@lastbrain/core/server";
2
2
  const jsonResponse = (payload, status = 200) => {
3
3
  return new Response(JSON.stringify(payload), {
4
4
  headers: {
5
- "content-type": "application/json"
5
+ "content-type": "application/json",
6
6
  },
7
- status
7
+ status,
8
8
  });
9
9
  };
10
10
  export async function POST(request) {
@@ -16,7 +16,7 @@ export async function POST(request) {
16
16
  }
17
17
  const { error, data } = await supabase.auth.signInWithPassword({
18
18
  email,
19
- password
19
+ password,
20
20
  });
21
21
  if (error) {
22
22
  return jsonResponse({ error: error.message }, 400);
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/api/storage.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhF;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAiBf"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/api/storage.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAiBf"}
@@ -41,7 +41,7 @@ export async function deleteFilesWithPrefix(bucket, prefix) {
41
41
  return;
42
42
  }
43
43
  if (files && files.length > 0) {
44
- const filePaths = files.map(file => file.name);
44
+ const filePaths = files.map((file) => file.name);
45
45
  await deleteFiles(bucket, filePaths);
46
46
  }
47
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"auth.build.config.d.ts","sourceRoot":"","sources":["../src/auth.build.config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,QAAA,MAAM,eAAe,EAAE,iBAoItB,CAAC;AAEF,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"auth.build.config.d.ts","sourceRoot":"","sources":["../src/auth.build.config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,QAAA,MAAM,eAAe,EAAE,iBAuJtB,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -66,12 +66,31 @@ const authBuildConfig = {
66
66
  entryPoint: "api/auth/profile",
67
67
  authRequired: true,
68
68
  },
69
+ {
70
+ method: "GET",
71
+ path: "/api/auth/me",
72
+ handlerExport: "GET",
73
+ entryPoint: "api/auth/me",
74
+ authRequired: true,
75
+ },
76
+ {
77
+ method: "GET",
78
+ path: "/api/admin/users",
79
+ handlerExport: "GET",
80
+ entryPoint: "api/admin/users",
81
+ authRequired: true,
82
+ },
69
83
  ],
70
84
  migrations: {
71
85
  enabled: true,
72
86
  priority: 20,
73
87
  path: "supabase/migrations",
74
- files: ["001_auth_base.sql"],
88
+ files: [
89
+ "20251112000000_user_init.sql",
90
+ "20251112000001_auto_profile_and_admin_view.sql",
91
+ "20251112000002_sync_avatars.sql",
92
+ ],
93
+ migrationsDownPath: "supabase/migrations-down",
75
94
  },
76
95
  menu: {
77
96
  public: [
@@ -95,7 +114,7 @@ const authBuildConfig = {
95
114
  title: "Gestion des utilisateurs",
96
115
  description: "Gérez les utilisateurs de la plateforme",
97
116
  icon: "Users2",
98
- path: "/admin/users",
117
+ path: "/admin/auth/users",
99
118
  order: 1,
100
119
  },
101
120
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"AA2CA,wBAAgB,cAAc,4CA8N7B"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"AAsDA,wBAAgB,cAAc,4CA6O7B"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useEffect, useState } from "react";
3
+ import { useCallback, useEffect, useState } from "react";
4
4
  import { Card, CardBody, CardHeader, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Spinner, Chip, Input, Button, Pagination, Avatar, addToast, } from "@lastbrain/ui";
5
5
  import { Users, Search, RefreshCw } from "lucide-react";
6
6
  export function AdminUsersPage() {
@@ -14,10 +14,7 @@ export function AdminUsersPage() {
14
14
  total: 0,
15
15
  total_pages: 0,
16
16
  });
17
- useEffect(() => {
18
- fetchUsers();
19
- }, [pagination.page]);
20
- const fetchUsers = async () => {
17
+ const fetchUsers = useCallback(async () => {
21
18
  try {
22
19
  setIsLoading(true);
23
20
  const params = new URLSearchParams({
@@ -58,7 +55,10 @@ export function AdminUsersPage() {
58
55
  finally {
59
56
  setIsLoading(false);
60
57
  }
61
- };
58
+ }, [pagination.page, pagination.per_page, searchQuery]);
59
+ useEffect(() => {
60
+ fetchUsers();
61
+ }, [fetchUsers]);
62
62
  const handleSearch = () => {
63
63
  setPagination((prev) => ({ ...prev, page: 1 }));
64
64
  fetchUsers();
@@ -80,10 +80,17 @@ export function AdminUsersPage() {
80
80
  if (e.key === "Enter") {
81
81
  handleSearch();
82
82
  }
83
- }, startContent: _jsx(Search, { className: "w-4 h-4 text-default-400" }), className: "flex-1" }), _jsx(Button, { color: "primary", onPress: handleSearch, isDisabled: isLoading, children: "Search" })] }), _jsx(Button, { variant: "flat", onPress: fetchUsers, isDisabled: isLoading, startContent: _jsx(RefreshCw, { className: "w-4 h-4" }), children: "Refresh" })] }) }), _jsx(CardBody, { children: isLoading ? (_jsx("div", { className: "flex justify-center items-center py-12", children: _jsx(Spinner, { size: "lg", label: "Loading users..." }) })) : users.length === 0 ? (_jsx("div", { className: "text-center py-12 text-default-500", children: "No users found" })) : (_jsxs(_Fragment, { children: [_jsxs(Table, { "aria-label": "Users table", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: "USER" }), _jsx(TableColumn, { children: "EMAIL" }), _jsx(TableColumn, { children: "COMPANY" }), _jsx(TableColumn, { children: "LOCATION" }), _jsx(TableColumn, { children: "CREATED" }), _jsx(TableColumn, { children: "STATUS" })] }), _jsx(TableBody, { children: users.map((user) => {
83
+ }, startContent: _jsx(Search, { className: "w-4 h-4 text-default-400" }), className: "flex-1" }), _jsx(Button, { color: "primary", onPress: handleSearch, isDisabled: isLoading, children: "Search" })] }), _jsx(Button, { variant: "flat", onPress: fetchUsers, isDisabled: isLoading, startContent: _jsx(RefreshCw, { className: "w-4 h-4" }), children: "Refresh" })] }) }), _jsx(CardBody, { children: isLoading ? (_jsx("div", { className: "flex justify-center items-center py-12", children: _jsx(Spinner, { size: "lg", label: "Loading users..." }) })) : users.length === 0 ? (_jsx("div", { className: "text-center py-12 text-default-500", children: "No users found" })) : (_jsxs(_Fragment, { children: [_jsxs(Table, { "aria-label": "Users table", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: "USER" }), _jsx(TableColumn, { children: "EMAIL" }), _jsx(TableColumn, { children: "ROLE" }), _jsx(TableColumn, { children: "COMPANY" }), _jsx(TableColumn, { children: "LAST SIGN IN" }), _jsx(TableColumn, { children: "CREATED" }), _jsx(TableColumn, { children: "STATUS" })] }), _jsx(TableBody, { children: users.map((user) => {
84
84
  const fullName = user.profile?.first_name && user.profile?.last_name
85
85
  ? `${user.profile.first_name} ${user.profile.last_name}`
86
- : "N/A";
87
- return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Avatar, { src: user.profile?.avatar_url, name: fullName, size: "sm" }), _jsx("span", { className: "text-small font-medium", children: fullName })] }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.email }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.profile?.company || "-" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.profile?.location || "-" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: formatDate(user.created_at) }) }), _jsx(TableCell, { children: _jsx(Chip, { color: "success", size: "sm", variant: "flat", children: "Active" }) })] }, user.id));
86
+ : user.full_name || "N/A";
87
+ const roleColor = user.role === "admin" || user.role === "superadmin"
88
+ ? "danger"
89
+ : user.role === "moderator"
90
+ ? "secondary"
91
+ : "default";
92
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Avatar, { src: `/api/storage/${user.profile?.avatar_url}`, name: fullName, size: "sm" }), _jsx("span", { className: "text-small font-medium", children: fullName })] }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.email }) }), _jsx(TableCell, { children: _jsx(Chip, { color: roleColor, size: "sm", variant: "flat", children: user.role || "user" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.profile?.company || "-" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.last_sign_in_at
93
+ ? formatDate(user.last_sign_in_at)
94
+ : "Never" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: formatDate(user.created_at) }) }), _jsx(TableCell, { children: _jsx(Chip, { color: "success", size: "sm", variant: "flat", children: "Active" }) })] }, user.id));
88
95
  }) })] }), pagination.total_pages > 1 && (_jsx("div", { className: "flex justify-center mt-4", children: _jsx(Pagination, { total: pagination.total_pages, page: pagination.page, onChange: handlePageChange, showControls: true }) })), _jsxs("div", { className: "mt-4 text-small text-default-500 text-center", children: ["Showing ", users.length, " of ", pagination.total, " users"] })] })) })] })] }));
89
96
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../../src/web/auth/dashboard.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,aAAa,mDA6K5B"}
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../../src/web/auth/dashboard.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,aAAa,mDAoL5B"}
@@ -28,7 +28,7 @@ export function DashboardPage() {
28
28
  }
29
29
  };
30
30
  if (isLoading) {
31
- return (_jsx("div", { className: "flex justify-center items-center min-h-[400px]", children: _jsx(Spinner, { size: "lg", label: "Loading dashboard..." }) }));
31
+ return (_jsx("div", { className: "flex justify-center items-center min-h-[400px]", children: _jsxs(Spinner, { color: "primary", size: "lg", children: [" ", _jsx("span", { className: "text-xs text-default-700", children: "Loading dashboard..." })] }) }));
32
32
  }
33
33
  if (error) {
34
34
  return (_jsx("div", { className: "pt-12", children: _jsx(Card, { className: "max-w-2xl mx-auto", children: _jsx(CardBody, { children: _jsxs("p", { className: "text-danger", children: ["Error: ", error] }) }) }) }));
@@ -39,6 +39,6 @@ export function DashboardPage() {
39
39
  const fullName = userData.profile?.first_name && userData.profile?.last_name
40
40
  ? `${userData.profile.first_name} ${userData.profile.last_name}`
41
41
  : "User";
42
- return (_jsxs("div", { className: "pt-12 pb-12 max-w-6xl mx-auto px-4", children: [_jsx("h1", { className: "text-3xl font-bold mb-8", children: "Dashboard" }), _jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [_jsxs(Card, { className: "col-span-full md:col-span-1", children: [_jsxs(CardHeader, { className: "flex gap-3", children: [_jsx(Avatar, { src: userData.profile?.avatar_url, icon: _jsx(User, {}), size: "lg", className: "flex-shrink-0" }), _jsxs("div", { className: "flex flex-col", children: [_jsx("p", { className: "text-xl font-semibold", children: fullName }), _jsx("p", { className: "text-small text-default-500", children: userData.email })] })] }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Mail, { className: "w-4 h-4 text-default-400" }), _jsx("span", { className: "text-small", children: userData.email })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { className: "w-4 h-4 text-default-400" }), _jsxs("span", { className: "text-small", children: ["Member since ", new Date(userData.created_at).toLocaleDateString()] })] }), userData.profile?.company && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Shield, { className: "w-4 h-4 text-default-400" }), _jsx("span", { className: "text-small", children: userData.profile.company })] }))] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Account Status" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex justify-between items-center", children: [_jsx("span", { className: "text-small", children: "Status" }), _jsx(Chip, { color: "success", size: "sm", variant: "flat", children: "Active" })] }), _jsxs("div", { className: "flex justify-between items-center", children: [_jsx("span", { className: "text-small", children: "Profile" }), _jsx(Chip, { color: userData.profile ? "success" : "warning", size: "sm", variant: "flat", children: userData.profile ? "Complete" : "Incomplete" })] })] }) })] }), userData.profile?.bio && (_jsxs(Card, { className: "col-span-full", children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Bio" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsx("p", { className: "text-small text-default-600", children: userData.profile.bio }) })] })), _jsxs(Card, { className: "col-span-full", children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Quick Stats" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [_jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-primary", children: "0" }), _jsx("p", { className: "text-small text-default-500", children: "Projects" })] }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-success", children: "0" }), _jsx("p", { className: "text-small text-default-500", children: "Tasks" })] }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-warning", children: "0" }), _jsx("p", { className: "text-small text-default-500", children: "Notifications" })] }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-secondary", children: Math.floor((Date.now() - new Date(userData.created_at).getTime()) /
42
+ return (_jsxs("div", { className: "pt-12 pb-12 max-w-6xl mx-auto px-4", children: [_jsx("h1", { className: "text-3xl font-bold mb-8", children: "Dashboard" }), _jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [_jsxs(Card, { className: "col-span-full md:col-span-1", children: [_jsxs(CardHeader, { className: "flex gap-3", children: [_jsx(Avatar, { src: userData.profile?.avatar_url, icon: _jsx(User, {}), size: "lg", className: "flex-shrink-0" }), _jsxs("div", { className: "flex flex-col", children: [_jsx("p", { className: "text-xl font-semibold", children: fullName }), _jsx("p", { className: "text-small text-default-500", children: userData.email })] })] }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Mail, { className: "w-4 h-4 text-default-400" }), _jsx("span", { className: "text-small", children: userData.email })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { className: "w-4 h-4 text-default-400" }), _jsxs("span", { className: "text-small", children: ["Member since", " ", new Date(userData.created_at).toLocaleDateString()] })] }), userData.profile?.company && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Shield, { className: "w-4 h-4 text-default-400" }), _jsx("span", { className: "text-small", children: userData.profile.company })] }))] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Account Status" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex justify-between items-center", children: [_jsx("span", { className: "text-small", children: "Status" }), _jsx(Chip, { color: "success", size: "sm", variant: "flat", children: "Active" })] }), _jsxs("div", { className: "flex justify-between items-center", children: [_jsx("span", { className: "text-small", children: "Profile" }), _jsx(Chip, { color: userData.profile ? "success" : "warning", size: "sm", variant: "flat", children: userData.profile ? "Complete" : "Incomplete" })] })] }) })] }), userData.profile?.bio && (_jsxs(Card, { className: "col-span-full", children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Bio" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsx("p", { className: "text-small text-default-600", children: userData.profile.bio }) })] })), _jsxs(Card, { className: "col-span-full", children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Quick Stats" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [_jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-primary", children: "0" }), _jsx("p", { className: "text-small text-default-500", children: "Projects" })] }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-success", children: "0" }), _jsx("p", { className: "text-small text-default-500", children: "Tasks" })] }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-warning", children: "0" }), _jsx("p", { className: "text-small text-default-500", children: "Notifications" })] }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-secondary", children: Math.floor((Date.now() - new Date(userData.created_at).getTime()) /
43
43
  (1000 * 60 * 60 * 24)) }), _jsx("p", { className: "text-small text-default-500", children: "Days active" })] })] }) })] })] })] }));
44
44
  }
@@ -1 +1 @@
1
- {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/web/auth/profile.tsx"],"names":[],"mappings":"AAyCA,wBAAgB,WAAW,4CAmV1B"}
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/web/auth/profile.tsx"],"names":[],"mappings":"AAyCA,wBAAgB,WAAW,4CA6X1B"}