@lastbrain/module-auth 0.1.2 → 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 +533 -0
- package/dist/api/admin/users.d.ts +9 -0
- package/dist/api/admin/users.d.ts.map +1 -0
- package/dist/api/admin/users.js +38 -0
- package/dist/api/auth/me.d.ts +17 -0
- package/dist/api/auth/me.d.ts.map +1 -0
- package/dist/api/auth/me.js +32 -0
- package/dist/api/auth/profile.d.ts +32 -0
- package/dist/api/auth/profile.d.ts.map +1 -0
- package/dist/api/auth/profile.js +104 -0
- package/dist/api/public/signin.js +3 -3
- package/dist/api/storage.d.ts +13 -0
- package/dist/api/storage.d.ts.map +1 -0
- package/dist/api/storage.js +47 -0
- package/dist/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +42 -2
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +94 -2
- package/dist/web/auth/dashboard.d.ts +1 -1
- package/dist/web/auth/dashboard.d.ts.map +1 -1
- package/dist/web/auth/dashboard.js +42 -2
- package/dist/web/auth/profile.d.ts.map +1 -1
- package/dist/web/auth/profile.js +191 -2
- package/dist/web/auth/reglage.d.ts.map +1 -1
- package/dist/web/auth/reglage.js +98 -2
- 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 +8 -7
- package/src/api/admin/users.ts +51 -0
- package/src/api/auth/me.ts +39 -0
- package/src/api/auth/profile.ts +142 -0
- package/src/api/public/signin.ts +3 -3
- package/src/api/storage.ts +66 -0
- package/src/auth.build.config.ts +42 -2
- package/src/web/admin/users.tsx +290 -1
- package/src/web/auth/dashboard.tsx +207 -1
- package/src/web/auth/profile.tsx +420 -1
- package/src/web/auth/reglage.tsx +284 -1
- 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/reglage.js
CHANGED
|
@@ -1,4 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Card, CardBody, CardHeader, Switch, Button, Spinner, Divider, Select, SelectItem, addToast, } from "@lastbrain/ui";
|
|
5
|
+
import { Settings, Save } from "lucide-react";
|
|
2
6
|
export function ReglagePage() {
|
|
3
|
-
|
|
7
|
+
const [preferences, setPreferences] = useState({
|
|
8
|
+
email_notifications: true,
|
|
9
|
+
push_notifications: false,
|
|
10
|
+
marketing_emails: false,
|
|
11
|
+
theme: "system",
|
|
12
|
+
language: "en",
|
|
13
|
+
timezone: "UTC",
|
|
14
|
+
});
|
|
15
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
16
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
fetchSettings();
|
|
19
|
+
}, []);
|
|
20
|
+
const fetchSettings = async () => {
|
|
21
|
+
try {
|
|
22
|
+
setIsLoading(true);
|
|
23
|
+
const response = await fetch("/api/auth/profile");
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error("Failed to fetch settings");
|
|
26
|
+
}
|
|
27
|
+
const result = await response.json();
|
|
28
|
+
if (result.data) {
|
|
29
|
+
const profile = result.data;
|
|
30
|
+
setPreferences((prev) => ({
|
|
31
|
+
...prev,
|
|
32
|
+
language: profile.language || prev.language,
|
|
33
|
+
timezone: profile.timezone || prev.timezone,
|
|
34
|
+
...(profile.preferences || {}),
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error("Error loading settings:", err);
|
|
40
|
+
addToast({
|
|
41
|
+
title: "Error",
|
|
42
|
+
description: "Failed to load settings",
|
|
43
|
+
color: "danger",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
setIsLoading(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const handleSave = async () => {
|
|
51
|
+
setIsSaving(true);
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch("/api/auth/profile", {
|
|
54
|
+
method: "PUT",
|
|
55
|
+
headers: {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
language: preferences.language,
|
|
60
|
+
timezone: preferences.timezone,
|
|
61
|
+
preferences: {
|
|
62
|
+
email_notifications: preferences.email_notifications,
|
|
63
|
+
push_notifications: preferences.push_notifications,
|
|
64
|
+
marketing_emails: preferences.marketing_emails,
|
|
65
|
+
theme: preferences.theme,
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error("Failed to update settings");
|
|
71
|
+
}
|
|
72
|
+
addToast({
|
|
73
|
+
title: "Success",
|
|
74
|
+
description: "Settings updated successfully",
|
|
75
|
+
color: "success",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error("Error updating settings:", err);
|
|
80
|
+
addToast({
|
|
81
|
+
title: "Error",
|
|
82
|
+
description: "Failed to update settings",
|
|
83
|
+
color: "danger",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
setIsSaving(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const handleToggle = (key, value) => {
|
|
91
|
+
setPreferences((prev) => ({ ...prev, [key]: value }));
|
|
92
|
+
};
|
|
93
|
+
const handleSelect = (key, value) => {
|
|
94
|
+
setPreferences((prev) => ({ ...prev, [key]: value }));
|
|
95
|
+
};
|
|
96
|
+
if (isLoading) {
|
|
97
|
+
return (_jsx("div", { className: "flex justify-center items-center min-h-[400px]", children: _jsx(Spinner, { size: "lg", label: "Loading settings..." }) }));
|
|
98
|
+
}
|
|
99
|
+
return (_jsxs("div", { className: "pt-12 pb-12 max-w-4xl mx-auto px-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-8", children: [_jsx(Settings, { className: "w-8 h-8" }), _jsx("h1", { className: "text-3xl font-bold", children: "Account Settings" })] }), _jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Notifications" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex justify-between items-center", children: [_jsxs("div", { children: [_jsx("p", { className: "font-medium", children: "Email Notifications" }), _jsx("p", { className: "text-small text-default-500", children: "Receive email notifications for important updates" })] }), _jsx(Switch, { isSelected: preferences.email_notifications, onValueChange: (value) => handleToggle("email_notifications", value) })] }), _jsx(Divider, {}), _jsxs("div", { className: "flex justify-between items-center", children: [_jsxs("div", { children: [_jsx("p", { className: "font-medium", children: "Push Notifications" }), _jsx("p", { className: "text-small text-default-500", children: "Receive push notifications in your browser" })] }), _jsx(Switch, { isSelected: preferences.push_notifications, onValueChange: (value) => handleToggle("push_notifications", value) })] }), _jsx(Divider, {}), _jsxs("div", { className: "flex justify-between items-center", children: [_jsxs("div", { children: [_jsx("p", { className: "font-medium", children: "Marketing Emails" }), _jsx("p", { className: "text-small text-default-500", children: "Receive emails about new features and updates" })] }), _jsx(Switch, { isSelected: preferences.marketing_emails, onValueChange: (value) => handleToggle("marketing_emails", value) })] })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Appearance" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs(Select, { label: "Theme", placeholder: "Select a theme", selectedKeys: preferences.theme ? [preferences.theme] : [], onChange: (e) => handleSelect("theme", e.target.value), children: [_jsx(SelectItem, { children: "Light" }, "light"), _jsx(SelectItem, { children: "Dark" }, "dark"), _jsx(SelectItem, { children: "System" }, "system")] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Language & Region" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs(Select, { label: "Language", placeholder: "Select a language", selectedKeys: preferences.language ? [preferences.language] : [], onChange: (e) => handleSelect("language", e.target.value), children: [_jsx(SelectItem, { children: "English" }, "en"), _jsx(SelectItem, { children: "Fran\u00E7ais" }, "fr"), _jsx(SelectItem, { children: "Espa\u00F1ol" }, "es"), _jsx(SelectItem, { children: "Deutsch" }, "de")] }), _jsxs(Select, { label: "Timezone", placeholder: "Select a timezone", selectedKeys: preferences.timezone ? [preferences.timezone] : [], onChange: (e) => handleSelect("timezone", e.target.value), children: [_jsx(SelectItem, { children: "UTC" }, "UTC"), _jsx(SelectItem, { children: "Europe/Paris" }, "Europe/Paris"), _jsx(SelectItem, { children: "America/New_York" }, "America/New_York"), _jsx(SelectItem, { children: "America/Los_Angeles" }, "America/Los_Angeles"), _jsx(SelectItem, { children: "Asia/Tokyo" }, "Asia/Tokyo")] })] }) })] }), _jsxs("div", { className: "flex justify-end gap-3", children: [_jsx(Button, { type: "button", variant: "flat", onPress: () => fetchSettings(), isDisabled: isSaving, children: "Reset" }), _jsx(Button, { type: "button", color: "primary", isLoading: isSaving, onPress: handleSave, startContent: !isSaving && _jsx(Save, { className: "w-4 h-4" }), children: isSaving ? "Saving..." : "Save Settings" })] })] })] }));
|
|
4
100
|
}
|
|
@@ -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",
|
|
@@ -28,10 +28,6 @@
|
|
|
28
28
|
"src",
|
|
29
29
|
"supabase"
|
|
30
30
|
],
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "tsc -p tsconfig.json",
|
|
33
|
-
"dev": "tsc -p tsconfig.json --watch"
|
|
34
|
-
},
|
|
35
31
|
"dependencies": {
|
|
36
32
|
"@lastbrain/core": "^0.1.0",
|
|
37
33
|
"@lastbrain/ui": "^0.1.4",
|
|
@@ -64,5 +60,10 @@
|
|
|
64
60
|
"default": "./dist/api/*.js"
|
|
65
61
|
}
|
|
66
62
|
},
|
|
67
|
-
"sideEffects": false
|
|
68
|
-
|
|
63
|
+
"sideEffects": false,
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsc -p tsconfig.json",
|
|
66
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
67
|
+
"lint": "eslint ."
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/admin/users
|
|
6
|
+
* Returns all users (superadmin only)
|
|
7
|
+
* Supports pagination via query params: page, per_page
|
|
8
|
+
* Supports search via query param: search (email)
|
|
9
|
+
*/
|
|
10
|
+
export async function GET(request: NextRequest) {
|
|
11
|
+
try {
|
|
12
|
+
const supabase = await getSupabaseServerClient();
|
|
13
|
+
|
|
14
|
+
// L'authentification et les droits superadmin sont déjà vérifiés par le middleware
|
|
15
|
+
// Get query parameters
|
|
16
|
+
const searchParams = request.nextUrl.searchParams;
|
|
17
|
+
const page = parseInt(searchParams.get("page") || "1");
|
|
18
|
+
const perPage = parseInt(searchParams.get("per_page") || "20");
|
|
19
|
+
const search = searchParams.get("search") || "";
|
|
20
|
+
|
|
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
|
+
);
|
|
30
|
+
|
|
31
|
+
if (usersError) {
|
|
32
|
+
console.error("Error fetching users:", usersError);
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ error: "Database Error", message: usersError.message },
|
|
35
|
+
{ status: 500 },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
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 } });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("Error in admin users endpoint:", error);
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{
|
|
45
|
+
error: "Internal Server Error",
|
|
46
|
+
message: "Failed to fetch users",
|
|
47
|
+
},
|
|
48
|
+
{ status: 500 },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/auth/me
|
|
6
|
+
* Returns the current authenticated user and their profile
|
|
7
|
+
*/
|
|
8
|
+
export async function GET() {
|
|
9
|
+
try {
|
|
10
|
+
const supabase = await getSupabaseServerClient();
|
|
11
|
+
|
|
12
|
+
// Get the authenticated user
|
|
13
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
14
|
+
|
|
15
|
+
// L'utilisateur est déjà authentifié grâce au middleware
|
|
16
|
+
// Get user profile
|
|
17
|
+
const { data: profile } = await supabase
|
|
18
|
+
.from("user_profil")
|
|
19
|
+
.select("*")
|
|
20
|
+
.eq("owner_id", user!.id)
|
|
21
|
+
.single();
|
|
22
|
+
|
|
23
|
+
// Profile might not exist yet, that's OK
|
|
24
|
+
const userData = {
|
|
25
|
+
id: user!.id,
|
|
26
|
+
email: user!.email,
|
|
27
|
+
created_at: user!.created_at,
|
|
28
|
+
profile: profile || null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return NextResponse.json({ data: userData });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error("Error fetching user:", error);
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: "Internal Server Error", message: "Failed to fetch user data" },
|
|
36
|
+
{ status: 500 },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/auth/profile
|
|
6
|
+
* Returns the user's profile
|
|
7
|
+
*/
|
|
8
|
+
export async function GET() {
|
|
9
|
+
try {
|
|
10
|
+
const supabase = await getSupabaseServerClient();
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
data: { user },
|
|
14
|
+
} = await supabase.auth.getUser();
|
|
15
|
+
|
|
16
|
+
// L'utilisateur est déjà authentifié grâce au middleware
|
|
17
|
+
const { data: profile, error: profileError } = await supabase
|
|
18
|
+
.from("user_profil")
|
|
19
|
+
.select("*")
|
|
20
|
+
.eq("owner_id", user!.id)
|
|
21
|
+
.single();
|
|
22
|
+
|
|
23
|
+
if (profileError && profileError.code !== "PGRST116") {
|
|
24
|
+
// PGRST116 = no rows returned, which is OK
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: "Database Error", message: profileError.message },
|
|
27
|
+
{ status: 500 },
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return NextResponse.json({ data: profile || null });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error("Error fetching profile:", error);
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: "Internal Server Error", message: "Failed to fetch profile" },
|
|
36
|
+
{ status: 500 },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* PUT /api/auth/profile
|
|
43
|
+
* Updates the user's profile
|
|
44
|
+
*/
|
|
45
|
+
export async function PUT(request: NextRequest) {
|
|
46
|
+
try {
|
|
47
|
+
const supabase = await getSupabaseServerClient();
|
|
48
|
+
|
|
49
|
+
const {
|
|
50
|
+
data: { user },
|
|
51
|
+
} = await supabase.auth.getUser();
|
|
52
|
+
|
|
53
|
+
// L'utilisateur est déjà authentifié grâce au middleware
|
|
54
|
+
const body = await request.json();
|
|
55
|
+
const {
|
|
56
|
+
first_name,
|
|
57
|
+
last_name,
|
|
58
|
+
avatar_url,
|
|
59
|
+
bio,
|
|
60
|
+
phone,
|
|
61
|
+
company,
|
|
62
|
+
website,
|
|
63
|
+
location,
|
|
64
|
+
language,
|
|
65
|
+
timezone,
|
|
66
|
+
preferences,
|
|
67
|
+
} = body;
|
|
68
|
+
|
|
69
|
+
// Check if profile exists
|
|
70
|
+
const { data: existingProfile } = await supabase
|
|
71
|
+
.from("user_profil")
|
|
72
|
+
.select("id")
|
|
73
|
+
.eq("owner_id", user!.id)
|
|
74
|
+
.single();
|
|
75
|
+
|
|
76
|
+
let result;
|
|
77
|
+
if (existingProfile) {
|
|
78
|
+
// Update existing profile
|
|
79
|
+
result = await supabase
|
|
80
|
+
.from("user_profil")
|
|
81
|
+
.update({
|
|
82
|
+
first_name,
|
|
83
|
+
last_name,
|
|
84
|
+
avatar_url,
|
|
85
|
+
bio,
|
|
86
|
+
phone,
|
|
87
|
+
company,
|
|
88
|
+
website,
|
|
89
|
+
location,
|
|
90
|
+
language,
|
|
91
|
+
timezone,
|
|
92
|
+
preferences,
|
|
93
|
+
})
|
|
94
|
+
.eq("owner_id", user!.id)
|
|
95
|
+
.select()
|
|
96
|
+
.single();
|
|
97
|
+
} else {
|
|
98
|
+
// Create new profile
|
|
99
|
+
result = await supabase
|
|
100
|
+
.from("user_profil")
|
|
101
|
+
.insert({
|
|
102
|
+
owner_id: user!.id,
|
|
103
|
+
first_name,
|
|
104
|
+
last_name,
|
|
105
|
+
avatar_url,
|
|
106
|
+
bio,
|
|
107
|
+
phone,
|
|
108
|
+
company,
|
|
109
|
+
website,
|
|
110
|
+
location,
|
|
111
|
+
language,
|
|
112
|
+
timezone,
|
|
113
|
+
preferences,
|
|
114
|
+
})
|
|
115
|
+
.select()
|
|
116
|
+
.single();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (result.error) {
|
|
120
|
+
return NextResponse.json(
|
|
121
|
+
{ error: "Database Error", message: result.error.message },
|
|
122
|
+
{ status: 500 },
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return NextResponse.json({ data: result.data });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error("Error updating profile:", error);
|
|
129
|
+
return NextResponse.json(
|
|
130
|
+
{ error: "Internal Server Error", message: "Failed to update profile" },
|
|
131
|
+
{ status: 500 },
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* PATCH /api/auth/profile
|
|
138
|
+
* Partially updates the user's profile
|
|
139
|
+
*/
|
|
140
|
+
export async function PATCH(request: NextRequest) {
|
|
141
|
+
return PUT(request);
|
|
142
|
+
}
|
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) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { supabaseBrowserClient } from "@lastbrain/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Upload a file to Supabase Storage and return proxy URL
|
|
5
|
+
*/
|
|
6
|
+
export async function uploadFile(
|
|
7
|
+
bucket: string,
|
|
8
|
+
path: string,
|
|
9
|
+
file: Blob,
|
|
10
|
+
contentType: string,
|
|
11
|
+
): Promise<string> {
|
|
12
|
+
const { data, error } = await supabaseBrowserClient.storage
|
|
13
|
+
.from(bucket)
|
|
14
|
+
.upload(path, file, {
|
|
15
|
+
contentType,
|
|
16
|
+
upsert: true,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (error) {
|
|
20
|
+
throw new Error(`Upload failed: ${error.message}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Return proxy URL instead of Supabase public URL
|
|
24
|
+
return `/api/storage/${bucket}/${data.path}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Delete files from Supabase Storage
|
|
29
|
+
*/
|
|
30
|
+
export async function deleteFiles(
|
|
31
|
+
bucket: string,
|
|
32
|
+
paths: string[],
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
const { error } = await supabaseBrowserClient.storage
|
|
35
|
+
.from(bucket)
|
|
36
|
+
.remove(paths);
|
|
37
|
+
|
|
38
|
+
if (error) {
|
|
39
|
+
throw new Error(`Delete failed: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Delete files starting with a specific prefix (like user ID)
|
|
45
|
+
*/
|
|
46
|
+
export async function deleteFilesWithPrefix(
|
|
47
|
+
bucket: string,
|
|
48
|
+
prefix: string,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
// List files with the prefix
|
|
51
|
+
const { data: files, error: listError } = await supabaseBrowserClient.storage
|
|
52
|
+
.from(bucket)
|
|
53
|
+
.list("", {
|
|
54
|
+
search: prefix,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (listError) {
|
|
58
|
+
console.warn("Failed to list files for deletion:", listError);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (files && files.length > 0) {
|
|
63
|
+
const filePaths = files.map((file) => file.name);
|
|
64
|
+
await deleteFiles(bucket, filePaths);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/auth.build.config.ts
CHANGED
|
@@ -47,12 +47,52 @@ const authBuildConfig: ModuleBuildConfig = {
|
|
|
47
47
|
entryPoint: "api/public/signin",
|
|
48
48
|
authRequired: false,
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
method: "GET",
|
|
52
|
+
path: "/api/auth/profile",
|
|
53
|
+
handlerExport: "GET",
|
|
54
|
+
entryPoint: "api/auth/profile",
|
|
55
|
+
authRequired: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
method: "PUT",
|
|
59
|
+
path: "/api/auth/profile",
|
|
60
|
+
handlerExport: "PUT",
|
|
61
|
+
entryPoint: "api/auth/profile",
|
|
62
|
+
authRequired: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
method: "PATCH",
|
|
66
|
+
path: "/api/auth/profile",
|
|
67
|
+
handlerExport: "PATCH",
|
|
68
|
+
entryPoint: "api/auth/profile",
|
|
69
|
+
authRequired: true,
|
|
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
|
+
},
|
|
50
85
|
],
|
|
51
86
|
migrations: {
|
|
52
87
|
enabled: true,
|
|
53
88
|
priority: 20,
|
|
54
89
|
path: "supabase/migrations",
|
|
55
|
-
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",
|
|
56
96
|
},
|
|
57
97
|
menu: {
|
|
58
98
|
public: [
|
|
@@ -76,7 +116,7 @@ const authBuildConfig: ModuleBuildConfig = {
|
|
|
76
116
|
title: "Gestion des utilisateurs",
|
|
77
117
|
description: "Gérez les utilisateurs de la plateforme",
|
|
78
118
|
icon: "Users2",
|
|
79
|
-
path: "/admin/users",
|
|
119
|
+
path: "/admin/auth/users",
|
|
80
120
|
order: 1,
|
|
81
121
|
},
|
|
82
122
|
],
|