@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.
- 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 +14 -63
- 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 +44 -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 +21 -89
- package/src/api/auth/me.ts +7 -14
- 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 +15 -8
- package/src/web/auth/profile.tsx +49 -7
- 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,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 [
|
|
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
|
-
|
|
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,
|
|
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.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
|
}
|
package/src/api/admin/users.ts
CHANGED
|
@@ -11,106 +11,38 @@ 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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
}
|
package/src/api/auth/me.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
19
|
+
const { data: profile } = await supabase
|
|
27
20
|
.from("user_profil")
|
|
28
21
|
.select("*")
|
|
29
|
-
.eq("owner_id", user
|
|
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
|
|
35
|
-
email: user
|
|
36
|
-
created_at: user
|
|
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
|
}
|
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, 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
|
-
|
|
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
|
)}
|
|
@@ -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>
|