@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
@@ -9,7 +9,8 @@ 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
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
14
  const [currentUser, setCurrentUser] = useState(null);
14
15
  useEffect(() => {
15
16
  fetchProfile();
@@ -108,8 +109,29 @@ export function ProfilePage() {
108
109
  },
109
110
  },
110
111
  });
111
- // Update profile avatar_url
112
- setProfile((prev) => ({ ...prev, avatar_url: urls.large }));
112
+ // Update profile avatar_url in database
113
+ try {
114
+ const response = await fetch("/api/auth/profile", {
115
+ method: "PATCH",
116
+ headers: {
117
+ "Content-Type": "application/json",
118
+ },
119
+ body: JSON.stringify({
120
+ avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
121
+ }),
122
+ });
123
+ if (!response.ok) {
124
+ console.error("Failed to update avatar_url in profile");
125
+ }
126
+ }
127
+ catch (error) {
128
+ console.error("Error updating profile avatar_url:", error);
129
+ }
130
+ // Update profile avatar_url locally
131
+ setProfile((prev) => ({
132
+ ...prev,
133
+ avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
134
+ }));
113
135
  return urls;
114
136
  };
115
137
  const handleAvatarDelete = async () => {
@@ -128,7 +150,25 @@ export function ProfilePage() {
128
150
  },
129
151
  },
130
152
  });
131
- // Update profile
153
+ // Update profile avatar_url in database
154
+ try {
155
+ const response = await fetch("/api/auth/profile", {
156
+ method: "PATCH",
157
+ headers: {
158
+ "Content-Type": "application/json",
159
+ },
160
+ body: JSON.stringify({
161
+ avatar_url: null,
162
+ }),
163
+ });
164
+ if (!response.ok) {
165
+ console.error("Failed to update avatar_url in profile");
166
+ }
167
+ }
168
+ catch (error) {
169
+ console.error("Error updating profile avatar_url:", error);
170
+ }
171
+ // Update profile locally
132
172
  setProfile((prev) => ({ ...prev, avatar_url: "" }));
133
173
  };
134
174
  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.5",
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,38 @@ 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),
39
+ // The RPC function returns the complete response with data and pagination
40
+ return NextResponse.json(
41
+ result || {
42
+ data: [],
43
+ pagination: { page, per_page: perPage, total: 0, total_pages: 0 },
112
44
  },
113
- });
45
+ );
114
46
  } catch (error) {
115
47
  console.error("Error in admin users endpoint:", error);
116
48
  return NextResponse.json(
@@ -118,7 +50,7 @@ export async function GET(request: NextRequest) {
118
50
  error: "Internal Server Error",
119
51
  message: "Failed to fetch users",
120
52
  },
121
- { status: 500 }
53
+ { status: 500 },
122
54
  );
123
55
  }
124
56
  }
@@ -12,28 +12,21 @@ export async function GET() {
12
12
  // Get the authenticated user
13
13
  const {
14
14
  data: { user },
15
- error: authError,
16
15
  } = await supabase.auth.getUser();
17
16
 
18
- if (authError || !user) {
19
- return NextResponse.json(
20
- { error: "Unauthorized", message: "User not authenticated" },
21
- { status: 401 }
22
- );
23
- }
24
-
17
+ // L'utilisateur est déjà authentifié grâce au middleware
25
18
  // Get user profile
26
- const { data: profile, error: profileError } = await supabase
19
+ const { data: profile } = await supabase
27
20
  .from("user_profil")
28
21
  .select("*")
29
- .eq("owner_id", user.id)
22
+ .eq("owner_id", user!.id)
30
23
  .single();
31
24
 
32
25
  // Profile might not exist yet, that's OK
33
26
  const userData = {
34
- id: user.id,
35
- email: user.email,
36
- created_at: user.created_at,
27
+ id: user!.id,
28
+ email: user!.email,
29
+ created_at: user!.created_at,
37
30
  profile: profile || null,
38
31
  };
39
32
 
@@ -42,7 +35,7 @@ export async function GET() {
42
35
  console.error("Error fetching user:", error);
43
36
  return NextResponse.json(
44
37
  { error: "Internal Server Error", message: "Failed to fetch user data" },
45
- { status: 500 }
38
+ { status: 500 },
46
39
  );
47
40
  }
48
41
  }
@@ -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, unknown>;
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
  )}
@@ -188,7 +195,7 @@ export function DashboardPage() {
188
195
  <p className="text-2xl font-bold text-secondary">
189
196
  {Math.floor(
190
197
  (Date.now() - new Date(userData.created_at).getTime()) /
191
- (1000 * 60 * 60 * 24)
198
+ (1000 * 60 * 60 * 24),
192
199
  )}
193
200
  </p>
194
201
  <p className="text-small text-default-500">Days active</p>