@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.
- package/README.md +38 -9
- package/dist/api/admin/users.d.ts +1 -28
- package/dist/api/admin/users.d.ts.map +1 -1
- package/dist/api/admin/users.js +12 -64
- package/dist/api/auth/me.d.ts +3 -3
- package/dist/api/auth/me.d.ts.map +1 -1
- package/dist/api/auth/me.js +3 -5
- package/dist/api/auth/profile.d.ts.map +1 -1
- package/dist/api/auth/profile.js +4 -8
- package/dist/api/public/signin.js +3 -3
- package/dist/api/storage.d.ts.map +1 -1
- package/dist/api/storage.js +1 -1
- package/dist/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +21 -2
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +16 -9
- package/dist/web/auth/dashboard.d.ts.map +1 -1
- package/dist/web/auth/dashboard.js +2 -2
- package/dist/web/auth/profile.d.ts.map +1 -1
- package/dist/web/auth/profile.js +43 -4
- package/dist/web/auth/reglage.d.ts.map +1 -1
- package/dist/web/public/SignInPage.d.ts.map +1 -1
- package/dist/web/public/SignInPage.js +1 -1
- package/dist/web/public/SignUpPage.js +1 -1
- package/package.json +3 -2
- package/src/api/admin/users.ts +17 -90
- package/src/api/auth/me.ts +8 -17
- package/src/api/auth/profile.ts +10 -24
- package/src/api/public/signin.ts +3 -3
- package/src/api/storage.ts +8 -5
- package/src/auth.build.config.ts +21 -2
- package/src/web/admin/users.tsx +37 -11
- package/src/web/auth/dashboard.tsx +14 -7
- package/src/web/auth/profile.tsx +45 -4
- package/src/web/auth/reglage.tsx +17 -35
- package/src/web/public/SignInPage.tsx +1 -2
- package/src/web/public/SignUpPage.tsx +2 -2
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/migrations/20251112000000_user_init.sql +1 -1
- package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +206 -0
- package/supabase/migrations/20251112000002_sync_avatars.sql +54 -0
- package/supabase/migrations-down/20251112000000_user_init.down.sql +2 -0
- package/supabase/migrations-down/20251112000001_auto_profile_and_admin_view.down.sql +23 -0
- package/supabase/migrations-down/20251112000002_sync_avatars.down.sql +9 -0
package/dist/web/auth/profile.js
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
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,
|
|
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":"
|
|
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,
|
|
13
|
+
const [error, _setError] = useState(null);
|
|
14
14
|
const redirectUrl = searchParams.get("redirect");
|
|
15
15
|
const router = useRouter();
|
|
16
16
|
const handleSubmit = async (event) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lastbrain/module-auth",
|
|
3
|
-
"version": "0.1.
|
|
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
|
}
|
package/src/api/admin/users.ts
CHANGED
|
@@ -11,106 +11,33 @@ export async function GET(request: NextRequest) {
|
|
|
11
11
|
try {
|
|
12
12
|
const supabase = await getSupabaseServerClient();
|
|
13
13
|
|
|
14
|
-
//
|
|
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
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 (
|
|
67
|
-
console.error("Error fetching users:",
|
|
31
|
+
if (usersError) {
|
|
32
|
+
console.error("Error fetching users:", usersError);
|
|
68
33
|
return NextResponse.json(
|
|
69
|
-
{ error: "Database Error", message:
|
|
70
|
-
{ status: 500 }
|
|
34
|
+
{ error: "Database Error", message: usersError.message },
|
|
35
|
+
{ status: 500 },
|
|
71
36
|
);
|
|
72
37
|
}
|
|
73
38
|
|
|
74
|
-
//
|
|
75
|
-
|
|
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
|
}
|
package/src/api/auth/me.ts
CHANGED
|
@@ -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
|
|
17
|
+
const { data: profile } = await supabase
|
|
27
18
|
.from("user_profil")
|
|
28
19
|
.select("*")
|
|
29
|
-
.eq("owner_id", user
|
|
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
|
|
35
|
-
email: user
|
|
36
|
-
created_at: user
|
|
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
|
}
|
package/src/api/auth/profile.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/api/public/signin.ts
CHANGED
|
@@ -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) {
|
package/src/api/storage.ts
CHANGED
|
@@ -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(
|
|
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
|
+
}
|
package/src/auth.build.config.ts
CHANGED
|
@@ -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: [
|
|
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
|
],
|
package/src/web/admin/users.tsx
CHANGED
|
@@ -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
|
-
|
|
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>
|
|
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.
|
|
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
|
|
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 =
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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">
|
|
167
|
+
<p className="text-small text-default-600">
|
|
168
|
+
{userData.profile.bio}
|
|
169
|
+
</p>
|
|
163
170
|
</CardBody>
|
|
164
171
|
</Card>
|
|
165
172
|
)}
|