@lastbrain/module-auth 0.1.3 → 0.1.4

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 +12 -64
  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 +43 -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 +17 -90
  27. package/src/api/auth/me.ts +8 -17
  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 +14 -7
  34. package/src/web/auth/profile.tsx +45 -4
  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
@@ -9,7 +9,7 @@ export function ProfilePage() {
9
9
  const [profile, setProfile] = useState({});
10
10
  const [isLoading, setIsLoading] = useState(true);
11
11
  const [isSaving, setIsSaving] = useState(false);
12
- const [error, setError] = useState(null);
12
+ const [_error, setError] = useState(null);
13
13
  const [currentUser, setCurrentUser] = useState(null);
14
14
  useEffect(() => {
15
15
  fetchProfile();
@@ -108,8 +108,29 @@ export function ProfilePage() {
108
108
  },
109
109
  },
110
110
  });
111
- // Update profile avatar_url
112
- setProfile((prev) => ({ ...prev, avatar_url: urls.large }));
111
+ // Update profile avatar_url in database
112
+ try {
113
+ const response = await fetch("/api/auth/profile", {
114
+ method: "PATCH",
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ },
118
+ body: JSON.stringify({
119
+ avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
120
+ }),
121
+ });
122
+ if (!response.ok) {
123
+ console.error("Failed to update avatar_url in profile");
124
+ }
125
+ }
126
+ catch (error) {
127
+ console.error("Error updating profile avatar_url:", error);
128
+ }
129
+ // Update profile avatar_url locally
130
+ setProfile((prev) => ({
131
+ ...prev,
132
+ avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
133
+ }));
113
134
  return urls;
114
135
  };
115
136
  const handleAvatarDelete = async () => {
@@ -128,7 +149,25 @@ export function ProfilePage() {
128
149
  },
129
150
  },
130
151
  });
131
- // Update profile
152
+ // Update profile avatar_url in database
153
+ try {
154
+ const response = await fetch("/api/auth/profile", {
155
+ method: "PATCH",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ },
159
+ body: JSON.stringify({
160
+ avatar_url: null,
161
+ }),
162
+ });
163
+ if (!response.ok) {
164
+ console.error("Failed to update avatar_url in profile");
165
+ }
166
+ }
167
+ catch (error) {
168
+ console.error("Error updating profile avatar_url:", error);
169
+ }
170
+ // Update profile locally
132
171
  setProfile((prev) => ({ ...prev, avatar_url: "" }));
133
172
  };
134
173
  if (isLoading) {
@@ -1 +1 @@
1
- {"version":3,"file":"reglage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/reglage.tsx"],"names":[],"mappings":"AAgCA,wBAAgB,WAAW,4CA+Q1B"}
1
+ {"version":3,"file":"reglage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/reglage.tsx"],"names":[],"mappings":"AAgCA,wBAAgB,WAAW,4CA6P1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"SignInPage.d.ts","sourceRoot":"","sources":["../../../src/web/public/SignInPage.tsx"],"names":[],"mappings":"AAwPA,wBAAgB,UAAU,4CAMzB"}
1
+ {"version":3,"file":"SignInPage.d.ts","sourceRoot":"","sources":["../../../src/web/public/SignInPage.tsx"],"names":[],"mappings":"AAuPA,wBAAgB,UAAU,4CAMzB"}
@@ -10,7 +10,7 @@ function SignInForm() {
10
10
  const [email, setEmail] = useState("");
11
11
  const [password, setPassword] = useState("");
12
12
  const [isLoading, setIsLoading] = useState(false);
13
- const [error, setError] = useState(null);
13
+ const [error, _setError] = useState(null);
14
14
  const redirectUrl = searchParams.get("redirect");
15
15
  const router = useRouter();
16
16
  const handleSubmit = async (event) => {
@@ -70,7 +70,7 @@ function SignUpForm() {
70
70
  }
71
71
  }, 2000);
72
72
  }
73
- catch (err) {
73
+ catch {
74
74
  addToast({
75
75
  title: "Erreur",
76
76
  description: "Une erreur inattendue est survenue.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/module-auth",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Module d'authentification complet pour LastBrain avec Supabase",
5
5
  "private": false,
6
6
  "type": "module",
@@ -63,6 +63,7 @@
63
63
  "sideEffects": false,
64
64
  "scripts": {
65
65
  "build": "tsc -p tsconfig.json",
66
- "dev": "tsc -p tsconfig.json --watch"
66
+ "dev": "tsc -p tsconfig.json --watch",
67
+ "lint": "eslint ."
67
68
  }
68
69
  }
@@ -11,106 +11,33 @@ export async function GET(request: NextRequest) {
11
11
  try {
12
12
  const supabase = await getSupabaseServerClient();
13
13
 
14
- // Check authentication
15
- const {
16
- data: { user },
17
- error: authError,
18
- } = await supabase.auth.getUser();
19
-
20
- if (authError || !user) {
21
- return NextResponse.json(
22
- { error: "Unauthorized", message: "User not authenticated" },
23
- { status: 401 }
24
- );
25
- }
26
-
27
- // Check if user is superadmin
28
- const { data: isSuperAdmin } = await supabase.rpc("is_superadmin", {
29
- user_id: user.id,
30
- });
31
-
32
- if (!isSuperAdmin) {
33
- return NextResponse.json(
34
- { error: "Forbidden", message: "Superadmin access required" },
35
- { status: 403 }
36
- );
37
- }
38
-
14
+ // L'authentification et les droits superadmin sont déjà vérifiés par le middleware
39
15
  // Get query parameters
40
16
  const searchParams = request.nextUrl.searchParams;
41
17
  const page = parseInt(searchParams.get("page") || "1");
42
18
  const perPage = parseInt(searchParams.get("per_page") || "20");
43
19
  const search = searchParams.get("search") || "";
44
20
 
45
- // Note: We can only get public user data from auth.users via RPC or service role
46
- // For now, we'll query user_profil which has owner_id references
47
- let query = supabase
48
- .from("user_profil")
49
- .select("*, owner_id", { count: "exact" })
50
- .order("created_at", { ascending: false });
51
-
52
- // Add search filter if provided - using Supabase's built-in parameterized query
53
- if (search) {
54
- // Using .ilike with % wildcards - Supabase handles escaping
55
- query = query.or(
56
- `first_name.ilike.%${search}%,last_name.ilike.%${search}%`
57
- );
58
- }
59
-
60
- // Apply pagination
61
- const start = (page - 1) * perPage;
62
- query = query.range(start, start + perPage - 1);
63
-
64
- const { data: profiles, error: profileError, count } = await query;
21
+ // Use RPC function to get users with emails
22
+ const { data: result, error: usersError } = await supabase.rpc(
23
+ "get_admin_users",
24
+ {
25
+ page_number: page,
26
+ page_size: perPage,
27
+ search_term: search,
28
+ }
29
+ );
65
30
 
66
- if (profileError) {
67
- console.error("Error fetching users:", profileError);
31
+ if (usersError) {
32
+ console.error("Error fetching users:", usersError);
68
33
  return NextResponse.json(
69
- { error: "Database Error", message: profileError.message },
70
- { status: 500 }
34
+ { error: "Database Error", message: usersError.message },
35
+ { status: 500 },
71
36
  );
72
37
  }
73
38
 
74
- // Get auth users data in batch - more efficient than individual calls
75
- // Note: auth.admin methods require service role, so results may be limited
76
- // In production, consider creating a database view or RPC function to join
77
- // user_profil with auth.users for better performance
78
- const users = await Promise.all(
79
- (profiles || []).map(async (profile) => {
80
- // Get basic auth info - this is limited to what's available
81
- const { data: authData } = await supabase.auth.admin.getUserById(
82
- profile.owner_id
83
- );
84
-
85
- return {
86
- id: profile.owner_id,
87
- email: authData?.user?.email || "N/A",
88
- created_at: authData?.user?.created_at || profile.created_at,
89
- profile: {
90
- first_name: profile.first_name,
91
- last_name: profile.last_name,
92
- avatar_url: profile.avatar_url,
93
- bio: profile.bio,
94
- phone: profile.phone,
95
- company: profile.company,
96
- website: profile.website,
97
- location: profile.location,
98
- language: profile.language,
99
- timezone: profile.timezone,
100
- },
101
- };
102
- })
103
- );
104
-
105
- return NextResponse.json({
106
- data: users,
107
- pagination: {
108
- page,
109
- per_page: perPage,
110
- total: count || 0,
111
- total_pages: Math.ceil((count || 0) / perPage),
112
- },
113
- });
39
+ // The RPC function returns the complete response with data and pagination
40
+ return NextResponse.json(result || { data: [], pagination: { page, per_page: perPage, total: 0, total_pages: 0 } });
114
41
  } catch (error) {
115
42
  console.error("Error in admin users endpoint:", error);
116
43
  return NextResponse.json(
@@ -118,7 +45,7 @@ export async function GET(request: NextRequest) {
118
45
  error: "Internal Server Error",
119
46
  message: "Failed to fetch users",
120
47
  },
121
- { status: 500 }
48
+ { status: 500 },
122
49
  );
123
50
  }
124
51
  }
@@ -10,30 +10,21 @@ export async function GET() {
10
10
  const supabase = await getSupabaseServerClient();
11
11
 
12
12
  // Get the authenticated user
13
- const {
14
- data: { user },
15
- error: authError,
16
- } = await supabase.auth.getUser();
17
-
18
- if (authError || !user) {
19
- return NextResponse.json(
20
- { error: "Unauthorized", message: "User not authenticated" },
21
- { status: 401 }
22
- );
23
- }
13
+ const { data: { user } } = await supabase.auth.getUser();
24
14
 
15
+ // L'utilisateur est déjà authentifié grâce au middleware
25
16
  // Get user profile
26
- const { data: profile, error: profileError } = await supabase
17
+ const { data: profile } = await supabase
27
18
  .from("user_profil")
28
19
  .select("*")
29
- .eq("owner_id", user.id)
20
+ .eq("owner_id", user!.id)
30
21
  .single();
31
22
 
32
23
  // Profile might not exist yet, that's OK
33
24
  const userData = {
34
- id: user.id,
35
- email: user.email,
36
- created_at: user.created_at,
25
+ id: user!.id,
26
+ email: user!.email,
27
+ created_at: user!.created_at,
37
28
  profile: profile || null,
38
29
  };
39
30
 
@@ -42,7 +33,7 @@ export async function GET() {
42
33
  console.error("Error fetching user:", error);
43
34
  return NextResponse.json(
44
35
  { error: "Internal Server Error", message: "Failed to fetch user data" },
45
- { status: 500 }
36
+ { status: 500 },
46
37
  );
47
38
  }
48
39
  }
@@ -11,27 +11,20 @@ export async function GET() {
11
11
 
12
12
  const {
13
13
  data: { user },
14
- error: authError,
15
14
  } = await supabase.auth.getUser();
16
15
 
17
- if (authError || !user) {
18
- return NextResponse.json(
19
- { error: "Unauthorized", message: "User not authenticated" },
20
- { status: 401 }
21
- );
22
- }
23
-
16
+ // L'utilisateur est déjà authentifié grâce au middleware
24
17
  const { data: profile, error: profileError } = await supabase
25
18
  .from("user_profil")
26
19
  .select("*")
27
- .eq("owner_id", user.id)
20
+ .eq("owner_id", user!.id)
28
21
  .single();
29
22
 
30
23
  if (profileError && profileError.code !== "PGRST116") {
31
24
  // PGRST116 = no rows returned, which is OK
32
25
  return NextResponse.json(
33
26
  { error: "Database Error", message: profileError.message },
34
- { status: 500 }
27
+ { status: 500 },
35
28
  );
36
29
  }
37
30
 
@@ -40,7 +33,7 @@ export async function GET() {
40
33
  console.error("Error fetching profile:", error);
41
34
  return NextResponse.json(
42
35
  { error: "Internal Server Error", message: "Failed to fetch profile" },
43
- { status: 500 }
36
+ { status: 500 },
44
37
  );
45
38
  }
46
39
  }
@@ -55,16 +48,9 @@ export async function PUT(request: NextRequest) {
55
48
 
56
49
  const {
57
50
  data: { user },
58
- error: authError,
59
51
  } = await supabase.auth.getUser();
60
52
 
61
- if (authError || !user) {
62
- return NextResponse.json(
63
- { error: "Unauthorized", message: "User not authenticated" },
64
- { status: 401 }
65
- );
66
- }
67
-
53
+ // L'utilisateur est déjà authentifié grâce au middleware
68
54
  const body = await request.json();
69
55
  const {
70
56
  first_name,
@@ -84,7 +70,7 @@ export async function PUT(request: NextRequest) {
84
70
  const { data: existingProfile } = await supabase
85
71
  .from("user_profil")
86
72
  .select("id")
87
- .eq("owner_id", user.id)
73
+ .eq("owner_id", user!.id)
88
74
  .single();
89
75
 
90
76
  let result;
@@ -105,7 +91,7 @@ export async function PUT(request: NextRequest) {
105
91
  timezone,
106
92
  preferences,
107
93
  })
108
- .eq("owner_id", user.id)
94
+ .eq("owner_id", user!.id)
109
95
  .select()
110
96
  .single();
111
97
  } else {
@@ -113,7 +99,7 @@ export async function PUT(request: NextRequest) {
113
99
  result = await supabase
114
100
  .from("user_profil")
115
101
  .insert({
116
- owner_id: user.id,
102
+ owner_id: user!.id,
117
103
  first_name,
118
104
  last_name,
119
105
  avatar_url,
@@ -133,7 +119,7 @@ export async function PUT(request: NextRequest) {
133
119
  if (result.error) {
134
120
  return NextResponse.json(
135
121
  { error: "Database Error", message: result.error.message },
136
- { status: 500 }
122
+ { status: 500 },
137
123
  );
138
124
  }
139
125
 
@@ -142,7 +128,7 @@ export async function PUT(request: NextRequest) {
142
128
  console.error("Error updating profile:", error);
143
129
  return NextResponse.json(
144
130
  { error: "Internal Server Error", message: "Failed to update profile" },
145
- { status: 500 }
131
+ { status: 500 },
146
132
  );
147
133
  }
148
134
  }
@@ -3,9 +3,9 @@ import { getSupabaseServerClient } from "@lastbrain/core/server";
3
3
  const jsonResponse = (payload: unknown, status = 200) => {
4
4
  return new Response(JSON.stringify(payload), {
5
5
  headers: {
6
- "content-type": "application/json"
6
+ "content-type": "application/json",
7
7
  },
8
- status
8
+ status,
9
9
  });
10
10
  };
11
11
 
@@ -20,7 +20,7 @@ export async function POST(request: Request) {
20
20
 
21
21
  const { error, data } = await supabase.auth.signInWithPassword({
22
22
  email,
23
- password
23
+ password,
24
24
  });
25
25
 
26
26
  if (error) {
@@ -7,7 +7,7 @@ export async function uploadFile(
7
7
  bucket: string,
8
8
  path: string,
9
9
  file: Blob,
10
- contentType: string
10
+ contentType: string,
11
11
  ): Promise<string> {
12
12
  const { data, error } = await supabaseBrowserClient.storage
13
13
  .from(bucket)
@@ -27,7 +27,10 @@ export async function uploadFile(
27
27
  /**
28
28
  * Delete files from Supabase Storage
29
29
  */
30
- export async function deleteFiles(bucket: string, paths: string[]): Promise<void> {
30
+ export async function deleteFiles(
31
+ bucket: string,
32
+ paths: string[],
33
+ ): Promise<void> {
31
34
  const { error } = await supabaseBrowserClient.storage
32
35
  .from(bucket)
33
36
  .remove(paths);
@@ -42,7 +45,7 @@ export async function deleteFiles(bucket: string, paths: string[]): Promise<void
42
45
  */
43
46
  export async function deleteFilesWithPrefix(
44
47
  bucket: string,
45
- prefix: string
48
+ prefix: string,
46
49
  ): Promise<void> {
47
50
  // List files with the prefix
48
51
  const { data: files, error: listError } = await supabaseBrowserClient.storage
@@ -57,7 +60,7 @@ export async function deleteFilesWithPrefix(
57
60
  }
58
61
 
59
62
  if (files && files.length > 0) {
60
- const filePaths = files.map(file => file.name);
63
+ const filePaths = files.map((file) => file.name);
61
64
  await deleteFiles(bucket, filePaths);
62
65
  }
63
- }
66
+ }
@@ -68,12 +68,31 @@ const authBuildConfig: ModuleBuildConfig = {
68
68
  entryPoint: "api/auth/profile",
69
69
  authRequired: true,
70
70
  },
71
+ {
72
+ method: "GET",
73
+ path: "/api/auth/me",
74
+ handlerExport: "GET",
75
+ entryPoint: "api/auth/me",
76
+ authRequired: true,
77
+ },
78
+ {
79
+ method: "GET",
80
+ path: "/api/admin/users",
81
+ handlerExport: "GET",
82
+ entryPoint: "api/admin/users",
83
+ authRequired: true,
84
+ },
71
85
  ],
72
86
  migrations: {
73
87
  enabled: true,
74
88
  priority: 20,
75
89
  path: "supabase/migrations",
76
- files: ["001_auth_base.sql"],
90
+ files: [
91
+ "20251112000000_user_init.sql",
92
+ "20251112000001_auto_profile_and_admin_view.sql",
93
+ "20251112000002_sync_avatars.sql"
94
+ ],
95
+ migrationsDownPath: "supabase/migrations-down",
77
96
  },
78
97
  menu: {
79
98
  public: [
@@ -97,7 +116,7 @@ const authBuildConfig: ModuleBuildConfig = {
97
116
  title: "Gestion des utilisateurs",
98
117
  description: "Gérez les utilisateurs de la plateforme",
99
118
  icon: "Users2",
100
- path: "/admin/users",
119
+ path: "/admin/auth/users",
101
120
  order: 1,
102
121
  },
103
122
  ],
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState } from "react";
3
+ import { useCallback, useEffect, useState } from "react";
4
4
  import {
5
5
  Card,
6
6
  CardBody,
@@ -25,12 +25,23 @@ interface User {
25
25
  id: string;
26
26
  email: string;
27
27
  created_at: string;
28
+ email_confirmed_at?: string;
29
+ last_sign_in_at?: string;
30
+ role?: string;
31
+ full_name?: string;
32
+ avatar_path?: string;
33
+ metadata?: Record<string, any>;
28
34
  profile: {
29
35
  first_name?: string;
30
36
  last_name?: string;
31
37
  avatar_url?: string;
32
38
  company?: string;
33
39
  location?: string;
40
+ bio?: string;
41
+ phone?: string;
42
+ website?: string;
43
+ language?: string;
44
+ timezone?: string;
34
45
  };
35
46
  }
36
47
 
@@ -53,11 +64,7 @@ export function AdminUsersPage() {
53
64
  total_pages: 0,
54
65
  });
55
66
 
56
- useEffect(() => {
57
- fetchUsers();
58
- }, [pagination.page]);
59
-
60
- const fetchUsers = async () => {
67
+ const fetchUsers = useCallback(async () => {
61
68
  try {
62
69
  setIsLoading(true);
63
70
  const params = new URLSearchParams({
@@ -101,7 +108,11 @@ export function AdminUsersPage() {
101
108
  } finally {
102
109
  setIsLoading(false);
103
110
  }
104
- };
111
+ }, [pagination.page, pagination.per_page, searchQuery]);
112
+
113
+ useEffect(() => {
114
+ fetchUsers();
115
+ }, [fetchUsers]);
105
116
 
106
117
  const handleSearch = () => {
107
118
  setPagination((prev) => ({ ...prev, page: 1 }));
@@ -188,8 +199,9 @@ export function AdminUsersPage() {
188
199
  <TableHeader>
189
200
  <TableColumn>USER</TableColumn>
190
201
  <TableColumn>EMAIL</TableColumn>
202
+ <TableColumn>ROLE</TableColumn>
191
203
  <TableColumn>COMPANY</TableColumn>
192
- <TableColumn>LOCATION</TableColumn>
204
+ <TableColumn>LAST SIGN IN</TableColumn>
193
205
  <TableColumn>CREATED</TableColumn>
194
206
  <TableColumn>STATUS</TableColumn>
195
207
  </TableHeader>
@@ -198,14 +210,21 @@ export function AdminUsersPage() {
198
210
  const fullName =
199
211
  user.profile?.first_name && user.profile?.last_name
200
212
  ? `${user.profile.first_name} ${user.profile.last_name}`
201
- : "N/A";
213
+ : user.full_name || "N/A";
214
+
215
+ const roleColor =
216
+ user.role === "admin" || user.role === "superadmin"
217
+ ? "danger"
218
+ : user.role === "moderator"
219
+ ? "secondary"
220
+ : "default";
202
221
 
203
222
  return (
204
223
  <TableRow key={user.id}>
205
224
  <TableCell>
206
225
  <div className="flex items-center gap-2">
207
226
  <Avatar
208
- src={user.profile?.avatar_url}
227
+ src={`/api/storage/${user.profile?.avatar_url}`}
209
228
  name={fullName}
210
229
  size="sm"
211
230
  />
@@ -217,6 +236,11 @@ export function AdminUsersPage() {
217
236
  <TableCell>
218
237
  <span className="text-small">{user.email}</span>
219
238
  </TableCell>
239
+ <TableCell>
240
+ <Chip color={roleColor} size="sm" variant="flat">
241
+ {user.role || "user"}
242
+ </Chip>
243
+ </TableCell>
220
244
  <TableCell>
221
245
  <span className="text-small">
222
246
  {user.profile?.company || "-"}
@@ -224,7 +248,9 @@ export function AdminUsersPage() {
224
248
  </TableCell>
225
249
  <TableCell>
226
250
  <span className="text-small">
227
- {user.profile?.location || "-"}
251
+ {user.last_sign_in_at
252
+ ? formatDate(user.last_sign_in_at)
253
+ : "Never"}
228
254
  </span>
229
255
  </TableCell>
230
256
  <TableCell>
@@ -39,7 +39,7 @@ export function DashboardPage() {
39
39
  try {
40
40
  setIsLoading(true);
41
41
  const response = await fetch("/api/auth/me");
42
-
42
+
43
43
  if (!response.ok) {
44
44
  throw new Error("Failed to fetch user data");
45
45
  }
@@ -56,7 +56,10 @@ export function DashboardPage() {
56
56
  if (isLoading) {
57
57
  return (
58
58
  <div className="flex justify-center items-center min-h-[400px]">
59
- <Spinner size="lg" label="Loading dashboard..." />
59
+ <Spinner color="primary" size="lg">
60
+ {" "}
61
+ <span className="text-xs text-default-700">Loading dashboard...</span>
62
+ </Spinner>
60
63
  </div>
61
64
  );
62
65
  }
@@ -77,9 +80,10 @@ export function DashboardPage() {
77
80
  return null;
78
81
  }
79
82
 
80
- const fullName = userData.profile?.first_name && userData.profile?.last_name
81
- ? `${userData.profile.first_name} ${userData.profile.last_name}`
82
- : "User";
83
+ const fullName =
84
+ userData.profile?.first_name && userData.profile?.last_name
85
+ ? `${userData.profile.first_name} ${userData.profile.last_name}`
86
+ : "User";
83
87
 
84
88
  return (
85
89
  <div className="pt-12 pb-12 max-w-6xl mx-auto px-4">
@@ -110,7 +114,8 @@ export function DashboardPage() {
110
114
  <div className="flex items-center gap-2">
111
115
  <Calendar className="w-4 h-4 text-default-400" />
112
116
  <span className="text-small">
113
- Member since {new Date(userData.created_at).toLocaleDateString()}
117
+ Member since{" "}
118
+ {new Date(userData.created_at).toLocaleDateString()}
114
119
  </span>
115
120
  </div>
116
121
  {userData.profile?.company && (
@@ -159,7 +164,9 @@ export function DashboardPage() {
159
164
  </CardHeader>
160
165
  <Divider />
161
166
  <CardBody>
162
- <p className="text-small text-default-600">{userData.profile.bio}</p>
167
+ <p className="text-small text-default-600">
168
+ {userData.profile.bio}
169
+ </p>
163
170
  </CardBody>
164
171
  </Card>
165
172
  )}