@lastbrain/module-auth 0.1.2 → 0.1.3
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 +504 -0
- package/dist/api/admin/users.d.ts +36 -0
- package/dist/api/admin/users.d.ts.map +1 -0
- package/dist/api/admin/users.js +90 -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 +34 -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 +108 -0
- 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 +21 -0
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +87 -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 +152 -2
- package/dist/web/auth/reglage.d.ts.map +1 -1
- package/dist/web/auth/reglage.js +98 -2
- package/package.json +7 -7
- package/src/api/admin/users.ts +124 -0
- package/src/api/auth/me.ts +48 -0
- package/src/api/auth/profile.ts +156 -0
- package/src/api/storage.ts +63 -0
- package/src/auth.build.config.ts +21 -0
- package/src/web/admin/users.tsx +264 -1
- package/src/web/auth/dashboard.tsx +200 -1
- package/src/web/auth/profile.tsx +379 -1
- package/src/web/auth/reglage.tsx +302 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* GET /api/auth/profile
|
|
4
|
+
* Returns the user's profile
|
|
5
|
+
*/
|
|
6
|
+
export declare function GET(): Promise<NextResponse<{
|
|
7
|
+
error: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}> | NextResponse<{
|
|
10
|
+
data: any;
|
|
11
|
+
}>>;
|
|
12
|
+
/**
|
|
13
|
+
* PUT /api/auth/profile
|
|
14
|
+
* Updates the user's profile
|
|
15
|
+
*/
|
|
16
|
+
export declare function PUT(request: NextRequest): Promise<NextResponse<{
|
|
17
|
+
error: string;
|
|
18
|
+
message: string;
|
|
19
|
+
}> | NextResponse<{
|
|
20
|
+
data: any;
|
|
21
|
+
}>>;
|
|
22
|
+
/**
|
|
23
|
+
* PATCH /api/auth/profile
|
|
24
|
+
* Partially updates the user's profile
|
|
25
|
+
*/
|
|
26
|
+
export declare function PATCH(request: NextRequest): Promise<NextResponse<{
|
|
27
|
+
error: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}> | NextResponse<{
|
|
30
|
+
data: any;
|
|
31
|
+
}>>;
|
|
32
|
+
//# sourceMappingURL=profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/api/auth/profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD;;;GAGG;AACH,wBAAsB,GAAG;;;;;IAsCxB;AAED;;;GAGG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;;IAgG7C;AAED;;;GAGG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,WAAW;;;;;IAE/C"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
3
|
+
/**
|
|
4
|
+
* GET /api/auth/profile
|
|
5
|
+
* Returns the user's profile
|
|
6
|
+
*/
|
|
7
|
+
export async function GET() {
|
|
8
|
+
try {
|
|
9
|
+
const supabase = await getSupabaseServerClient();
|
|
10
|
+
const { data: { user }, error: authError, } = await supabase.auth.getUser();
|
|
11
|
+
if (authError || !user) {
|
|
12
|
+
return NextResponse.json({ error: "Unauthorized", message: "User not authenticated" }, { status: 401 });
|
|
13
|
+
}
|
|
14
|
+
const { data: profile, error: profileError } = await supabase
|
|
15
|
+
.from("user_profil")
|
|
16
|
+
.select("*")
|
|
17
|
+
.eq("owner_id", user.id)
|
|
18
|
+
.single();
|
|
19
|
+
if (profileError && profileError.code !== "PGRST116") {
|
|
20
|
+
// PGRST116 = no rows returned, which is OK
|
|
21
|
+
return NextResponse.json({ error: "Database Error", message: profileError.message }, { status: 500 });
|
|
22
|
+
}
|
|
23
|
+
return NextResponse.json({ data: profile || null });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error("Error fetching profile:", error);
|
|
27
|
+
return NextResponse.json({ error: "Internal Server Error", message: "Failed to fetch profile" }, { status: 500 });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* PUT /api/auth/profile
|
|
32
|
+
* Updates the user's profile
|
|
33
|
+
*/
|
|
34
|
+
export async function PUT(request) {
|
|
35
|
+
try {
|
|
36
|
+
const supabase = await getSupabaseServerClient();
|
|
37
|
+
const { data: { user }, error: authError, } = await supabase.auth.getUser();
|
|
38
|
+
if (authError || !user) {
|
|
39
|
+
return NextResponse.json({ error: "Unauthorized", message: "User not authenticated" }, { status: 401 });
|
|
40
|
+
}
|
|
41
|
+
const body = await request.json();
|
|
42
|
+
const { first_name, last_name, avatar_url, bio, phone, company, website, location, language, timezone, preferences, } = body;
|
|
43
|
+
// Check if profile exists
|
|
44
|
+
const { data: existingProfile } = await supabase
|
|
45
|
+
.from("user_profil")
|
|
46
|
+
.select("id")
|
|
47
|
+
.eq("owner_id", user.id)
|
|
48
|
+
.single();
|
|
49
|
+
let result;
|
|
50
|
+
if (existingProfile) {
|
|
51
|
+
// Update existing profile
|
|
52
|
+
result = await supabase
|
|
53
|
+
.from("user_profil")
|
|
54
|
+
.update({
|
|
55
|
+
first_name,
|
|
56
|
+
last_name,
|
|
57
|
+
avatar_url,
|
|
58
|
+
bio,
|
|
59
|
+
phone,
|
|
60
|
+
company,
|
|
61
|
+
website,
|
|
62
|
+
location,
|
|
63
|
+
language,
|
|
64
|
+
timezone,
|
|
65
|
+
preferences,
|
|
66
|
+
})
|
|
67
|
+
.eq("owner_id", user.id)
|
|
68
|
+
.select()
|
|
69
|
+
.single();
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Create new profile
|
|
73
|
+
result = await supabase
|
|
74
|
+
.from("user_profil")
|
|
75
|
+
.insert({
|
|
76
|
+
owner_id: user.id,
|
|
77
|
+
first_name,
|
|
78
|
+
last_name,
|
|
79
|
+
avatar_url,
|
|
80
|
+
bio,
|
|
81
|
+
phone,
|
|
82
|
+
company,
|
|
83
|
+
website,
|
|
84
|
+
location,
|
|
85
|
+
language,
|
|
86
|
+
timezone,
|
|
87
|
+
preferences,
|
|
88
|
+
})
|
|
89
|
+
.select()
|
|
90
|
+
.single();
|
|
91
|
+
}
|
|
92
|
+
if (result.error) {
|
|
93
|
+
return NextResponse.json({ error: "Database Error", message: result.error.message }, { status: 500 });
|
|
94
|
+
}
|
|
95
|
+
return NextResponse.json({ data: result.data });
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error("Error updating profile:", error);
|
|
99
|
+
return NextResponse.json({ error: "Internal Server Error", message: "Failed to update profile" }, { status: 500 });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* PATCH /api/auth/profile
|
|
104
|
+
* Partially updates the user's profile
|
|
105
|
+
*/
|
|
106
|
+
export async function PATCH(request) {
|
|
107
|
+
return PUT(request);
|
|
108
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload a file to Supabase Storage and return proxy URL
|
|
3
|
+
*/
|
|
4
|
+
export declare function uploadFile(bucket: string, path: string, file: Blob, contentType: string): Promise<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Delete files from Supabase Storage
|
|
7
|
+
*/
|
|
8
|
+
export declare function deleteFiles(bucket: string, paths: string[]): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Delete files starting with a specific prefix (like user ID)
|
|
11
|
+
*/
|
|
12
|
+
export declare function deleteFilesWithPrefix(bucket: string, prefix: string): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/api/storage.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhF;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { supabaseBrowserClient } from "@lastbrain/core";
|
|
2
|
+
/**
|
|
3
|
+
* Upload a file to Supabase Storage and return proxy URL
|
|
4
|
+
*/
|
|
5
|
+
export async function uploadFile(bucket, path, file, contentType) {
|
|
6
|
+
const { data, error } = await supabaseBrowserClient.storage
|
|
7
|
+
.from(bucket)
|
|
8
|
+
.upload(path, file, {
|
|
9
|
+
contentType,
|
|
10
|
+
upsert: true,
|
|
11
|
+
});
|
|
12
|
+
if (error) {
|
|
13
|
+
throw new Error(`Upload failed: ${error.message}`);
|
|
14
|
+
}
|
|
15
|
+
// Return proxy URL instead of Supabase public URL
|
|
16
|
+
return `/api/storage/${bucket}/${data.path}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Delete files from Supabase Storage
|
|
20
|
+
*/
|
|
21
|
+
export async function deleteFiles(bucket, paths) {
|
|
22
|
+
const { error } = await supabaseBrowserClient.storage
|
|
23
|
+
.from(bucket)
|
|
24
|
+
.remove(paths);
|
|
25
|
+
if (error) {
|
|
26
|
+
throw new Error(`Delete failed: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Delete files starting with a specific prefix (like user ID)
|
|
31
|
+
*/
|
|
32
|
+
export async function deleteFilesWithPrefix(bucket, prefix) {
|
|
33
|
+
// List files with the prefix
|
|
34
|
+
const { data: files, error: listError } = await supabaseBrowserClient.storage
|
|
35
|
+
.from(bucket)
|
|
36
|
+
.list("", {
|
|
37
|
+
search: prefix,
|
|
38
|
+
});
|
|
39
|
+
if (listError) {
|
|
40
|
+
console.warn("Failed to list files for deletion:", listError);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (files && files.length > 0) {
|
|
44
|
+
const filePaths = files.map(file => file.name);
|
|
45
|
+
await deleteFiles(bucket, filePaths);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.build.config.d.ts","sourceRoot":"","sources":["../src/auth.build.config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,QAAA,MAAM,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"auth.build.config.d.ts","sourceRoot":"","sources":["../src/auth.build.config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,QAAA,MAAM,eAAe,EAAE,iBAoItB,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -45,6 +45,27 @@ const authBuildConfig = {
|
|
|
45
45
|
entryPoint: "api/public/signin",
|
|
46
46
|
authRequired: false,
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
method: "GET",
|
|
50
|
+
path: "/api/auth/profile",
|
|
51
|
+
handlerExport: "GET",
|
|
52
|
+
entryPoint: "api/auth/profile",
|
|
53
|
+
authRequired: true,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
method: "PUT",
|
|
57
|
+
path: "/api/auth/profile",
|
|
58
|
+
handlerExport: "PUT",
|
|
59
|
+
entryPoint: "api/auth/profile",
|
|
60
|
+
authRequired: true,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
method: "PATCH",
|
|
64
|
+
path: "/api/auth/profile",
|
|
65
|
+
handlerExport: "PATCH",
|
|
66
|
+
entryPoint: "api/auth/profile",
|
|
67
|
+
authRequired: true,
|
|
68
|
+
},
|
|
48
69
|
],
|
|
49
70
|
migrations: {
|
|
50
71
|
enabled: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"AA2CA,wBAAgB,cAAc,4CA8N7B"}
|
package/dist/web/admin/users.js
CHANGED
|
@@ -1,4 +1,89 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Card, CardBody, CardHeader, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Spinner, Chip, Input, Button, Pagination, Avatar, addToast, } from "@lastbrain/ui";
|
|
5
|
+
import { Users, Search, RefreshCw } from "lucide-react";
|
|
2
6
|
export function AdminUsersPage() {
|
|
3
|
-
|
|
7
|
+
const [users, setUsers] = useState([]);
|
|
8
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
9
|
+
const [error, setError] = useState(null);
|
|
10
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
11
|
+
const [pagination, setPagination] = useState({
|
|
12
|
+
page: 1,
|
|
13
|
+
per_page: 20,
|
|
14
|
+
total: 0,
|
|
15
|
+
total_pages: 0,
|
|
16
|
+
});
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
fetchUsers();
|
|
19
|
+
}, [pagination.page]);
|
|
20
|
+
const fetchUsers = async () => {
|
|
21
|
+
try {
|
|
22
|
+
setIsLoading(true);
|
|
23
|
+
const params = new URLSearchParams({
|
|
24
|
+
page: pagination.page.toString(),
|
|
25
|
+
per_page: pagination.per_page.toString(),
|
|
26
|
+
});
|
|
27
|
+
if (searchQuery) {
|
|
28
|
+
params.append("search", searchQuery);
|
|
29
|
+
}
|
|
30
|
+
const response = await fetch(`/api/admin/users?${params}`);
|
|
31
|
+
if (response.status === 403) {
|
|
32
|
+
setError("You don't have permission to access this page");
|
|
33
|
+
addToast({
|
|
34
|
+
title: "Access Denied",
|
|
35
|
+
description: "Superadmin access required",
|
|
36
|
+
color: "danger",
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error("Failed to fetch users");
|
|
42
|
+
}
|
|
43
|
+
const result = await response.json();
|
|
44
|
+
setUsers(result.data || []);
|
|
45
|
+
if (result.pagination) {
|
|
46
|
+
setPagination(result.pagination);
|
|
47
|
+
}
|
|
48
|
+
setError(null);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
52
|
+
addToast({
|
|
53
|
+
title: "Error",
|
|
54
|
+
description: "Failed to load users",
|
|
55
|
+
color: "danger",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const handleSearch = () => {
|
|
63
|
+
setPagination((prev) => ({ ...prev, page: 1 }));
|
|
64
|
+
fetchUsers();
|
|
65
|
+
};
|
|
66
|
+
const handlePageChange = (page) => {
|
|
67
|
+
setPagination((prev) => ({ ...prev, page }));
|
|
68
|
+
};
|
|
69
|
+
const formatDate = (dateString) => {
|
|
70
|
+
return new Date(dateString).toLocaleDateString("en-US", {
|
|
71
|
+
year: "numeric",
|
|
72
|
+
month: "short",
|
|
73
|
+
day: "numeric",
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
if (error && users.length === 0) {
|
|
77
|
+
return (_jsx("div", { className: "pt-12 pb-12 max-w-7xl mx-auto px-4", children: _jsx(Card, { children: _jsx(CardBody, { children: _jsx("p", { className: "text-danger", children: error }) }) }) }));
|
|
78
|
+
}
|
|
79
|
+
return (_jsxs("div", { className: "pt-12 pb-12 max-w-7xl mx-auto px-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-8", children: [_jsx(Users, { className: "w-8 h-8" }), _jsx("h1", { className: "text-3xl font-bold", children: "User Management" })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex flex-col md:flex-row gap-4 w-full", children: [_jsxs("div", { className: "flex gap-2 flex-1", children: [_jsx(Input, { placeholder: "Search by email or name...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onKeyPress: (e) => {
|
|
80
|
+
if (e.key === "Enter") {
|
|
81
|
+
handleSearch();
|
|
82
|
+
}
|
|
83
|
+
}, startContent: _jsx(Search, { className: "w-4 h-4 text-default-400" }), className: "flex-1" }), _jsx(Button, { color: "primary", onPress: handleSearch, isDisabled: isLoading, children: "Search" })] }), _jsx(Button, { variant: "flat", onPress: fetchUsers, isDisabled: isLoading, startContent: _jsx(RefreshCw, { className: "w-4 h-4" }), children: "Refresh" })] }) }), _jsx(CardBody, { children: isLoading ? (_jsx("div", { className: "flex justify-center items-center py-12", children: _jsx(Spinner, { size: "lg", label: "Loading users..." }) })) : users.length === 0 ? (_jsx("div", { className: "text-center py-12 text-default-500", children: "No users found" })) : (_jsxs(_Fragment, { children: [_jsxs(Table, { "aria-label": "Users table", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: "USER" }), _jsx(TableColumn, { children: "EMAIL" }), _jsx(TableColumn, { children: "COMPANY" }), _jsx(TableColumn, { children: "LOCATION" }), _jsx(TableColumn, { children: "CREATED" }), _jsx(TableColumn, { children: "STATUS" })] }), _jsx(TableBody, { children: users.map((user) => {
|
|
84
|
+
const fullName = user.profile?.first_name && user.profile?.last_name
|
|
85
|
+
? `${user.profile.first_name} ${user.profile.last_name}`
|
|
86
|
+
: "N/A";
|
|
87
|
+
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Avatar, { src: user.profile?.avatar_url, name: fullName, size: "sm" }), _jsx("span", { className: "text-small font-medium", children: fullName })] }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.email }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.profile?.company || "-" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.profile?.location || "-" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: formatDate(user.created_at) }) }), _jsx(TableCell, { children: _jsx(Chip, { color: "success", size: "sm", variant: "flat", children: "Active" }) })] }, user.id));
|
|
88
|
+
}) })] }), pagination.total_pages > 1 && (_jsx("div", { className: "flex justify-center mt-4", children: _jsx(Pagination, { total: pagination.total_pages, page: pagination.page, onChange: handlePageChange, showControls: true }) })), _jsxs("div", { className: "mt-4 text-small text-default-500 text-center", children: ["Showing ", users.length, " of ", pagination.total, " users"] })] })) })] })] }));
|
|
4
89
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function DashboardPage(): import("react/jsx-runtime").JSX.Element;
|
|
1
|
+
export declare function DashboardPage(): import("react/jsx-runtime").JSX.Element | null;
|
|
2
2
|
//# sourceMappingURL=dashboard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../../src/web/auth/dashboard.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../../src/web/auth/dashboard.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,aAAa,mDA6K5B"}
|
|
@@ -1,4 +1,44 @@
|
|
|
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, Spinner, Chip, Divider, Avatar, } from "@lastbrain/ui";
|
|
5
|
+
import { User, Mail, Calendar, Shield } from "lucide-react";
|
|
2
6
|
export function DashboardPage() {
|
|
3
|
-
|
|
7
|
+
const [userData, setUserData] = useState(null);
|
|
8
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
9
|
+
const [error, setError] = useState(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
fetchUserData();
|
|
12
|
+
}, []);
|
|
13
|
+
const fetchUserData = async () => {
|
|
14
|
+
try {
|
|
15
|
+
setIsLoading(true);
|
|
16
|
+
const response = await fetch("/api/auth/me");
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
throw new Error("Failed to fetch user data");
|
|
19
|
+
}
|
|
20
|
+
const result = await response.json();
|
|
21
|
+
setUserData(result.data);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
setIsLoading(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
if (isLoading) {
|
|
31
|
+
return (_jsx("div", { className: "flex justify-center items-center min-h-[400px]", children: _jsx(Spinner, { size: "lg", label: "Loading dashboard..." }) }));
|
|
32
|
+
}
|
|
33
|
+
if (error) {
|
|
34
|
+
return (_jsx("div", { className: "pt-12", children: _jsx(Card, { className: "max-w-2xl mx-auto", children: _jsx(CardBody, { children: _jsxs("p", { className: "text-danger", children: ["Error: ", error] }) }) }) }));
|
|
35
|
+
}
|
|
36
|
+
if (!userData) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const fullName = userData.profile?.first_name && userData.profile?.last_name
|
|
40
|
+
? `${userData.profile.first_name} ${userData.profile.last_name}`
|
|
41
|
+
: "User";
|
|
42
|
+
return (_jsxs("div", { className: "pt-12 pb-12 max-w-6xl mx-auto px-4", children: [_jsx("h1", { className: "text-3xl font-bold mb-8", children: "Dashboard" }), _jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [_jsxs(Card, { className: "col-span-full md:col-span-1", children: [_jsxs(CardHeader, { className: "flex gap-3", children: [_jsx(Avatar, { src: userData.profile?.avatar_url, icon: _jsx(User, {}), size: "lg", className: "flex-shrink-0" }), _jsxs("div", { className: "flex flex-col", children: [_jsx("p", { className: "text-xl font-semibold", children: fullName }), _jsx("p", { className: "text-small text-default-500", children: userData.email })] })] }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Mail, { className: "w-4 h-4 text-default-400" }), _jsx("span", { className: "text-small", children: userData.email })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { className: "w-4 h-4 text-default-400" }), _jsxs("span", { className: "text-small", children: ["Member since ", new Date(userData.created_at).toLocaleDateString()] })] }), userData.profile?.company && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Shield, { className: "w-4 h-4 text-default-400" }), _jsx("span", { className: "text-small", children: userData.profile.company })] }))] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Account Status" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex justify-between items-center", children: [_jsx("span", { className: "text-small", children: "Status" }), _jsx(Chip, { color: "success", size: "sm", variant: "flat", children: "Active" })] }), _jsxs("div", { className: "flex justify-between items-center", children: [_jsx("span", { className: "text-small", children: "Profile" }), _jsx(Chip, { color: userData.profile ? "success" : "warning", size: "sm", variant: "flat", children: userData.profile ? "Complete" : "Incomplete" })] })] }) })] }), userData.profile?.bio && (_jsxs(Card, { className: "col-span-full", children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Bio" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsx("p", { className: "text-small text-default-600", children: userData.profile.bio }) })] })), _jsxs(Card, { className: "col-span-full", children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Quick Stats" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [_jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-primary", children: "0" }), _jsx("p", { className: "text-small text-default-500", children: "Projects" })] }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-success", children: "0" }), _jsx("p", { className: "text-small text-default-500", children: "Tasks" })] }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-warning", children: "0" }), _jsx("p", { className: "text-small text-default-500", children: "Notifications" })] }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-2xl font-bold text-secondary", children: Math.floor((Date.now() - new Date(userData.created_at).getTime()) /
|
|
43
|
+
(1000 * 60 * 60 * 24)) }), _jsx("p", { className: "text-small text-default-500", children: "Days active" })] })] }) })] })] })] }));
|
|
4
44
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/web/auth/profile.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/web/auth/profile.tsx"],"names":[],"mappings":"AAyCA,wBAAgB,WAAW,4CAmV1B"}
|
package/dist/web/auth/profile.js
CHANGED
|
@@ -1,4 +1,154 @@
|
|
|
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, Input, Textarea, Button, Spinner, Divider, addToast, AvatarUploader, } from "@lastbrain/ui";
|
|
5
|
+
import { Save, User } from "lucide-react";
|
|
6
|
+
import { uploadFile, deleteFilesWithPrefix } from "../../api/storage.js";
|
|
7
|
+
import { supabaseBrowserClient } from "@lastbrain/core";
|
|
2
8
|
export function ProfilePage() {
|
|
3
|
-
|
|
9
|
+
const [profile, setProfile] = useState({});
|
|
10
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
11
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const [currentUser, setCurrentUser] = useState(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
fetchProfile();
|
|
16
|
+
fetchCurrentUser();
|
|
17
|
+
}, []);
|
|
18
|
+
const fetchCurrentUser = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const { data: { user }, } = await supabaseBrowserClient.auth.getUser();
|
|
21
|
+
setCurrentUser(user);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error("Error fetching current user:", err);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const fetchProfile = async () => {
|
|
28
|
+
try {
|
|
29
|
+
setIsLoading(true);
|
|
30
|
+
const response = await fetch("/api/auth/profile");
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error("Failed to fetch profile");
|
|
33
|
+
}
|
|
34
|
+
const result = await response.json();
|
|
35
|
+
if (result.data) {
|
|
36
|
+
setProfile(result.data);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
41
|
+
addToast({
|
|
42
|
+
title: "Error",
|
|
43
|
+
description: "Failed to load profile",
|
|
44
|
+
color: "danger",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
setIsLoading(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const handleSubmit = async (e) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
setIsSaving(true);
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch("/api/auth/profile", {
|
|
56
|
+
method: "PUT",
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify(profile),
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error("Failed to update profile");
|
|
64
|
+
}
|
|
65
|
+
addToast({
|
|
66
|
+
title: "Success",
|
|
67
|
+
description: "Profile updated successfully",
|
|
68
|
+
color: "success",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error("Error updating profile:", err);
|
|
73
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
74
|
+
addToast({
|
|
75
|
+
title: "Error",
|
|
76
|
+
description: "Failed to update profile",
|
|
77
|
+
color: "danger",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
setIsSaving(false);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const handleChange = (field, value) => {
|
|
85
|
+
setProfile((prev) => ({ ...prev, [field]: value }));
|
|
86
|
+
};
|
|
87
|
+
const handleAvatarUpload = async (files) => {
|
|
88
|
+
if (!currentUser)
|
|
89
|
+
throw new Error("User not authenticated");
|
|
90
|
+
const version = Date.now();
|
|
91
|
+
const urls = {
|
|
92
|
+
small: "",
|
|
93
|
+
medium: "",
|
|
94
|
+
large: "",
|
|
95
|
+
};
|
|
96
|
+
// Upload all three sizes
|
|
97
|
+
urls.small = await uploadFile("avatar", `${currentUser.id}_32_${version}.webp`, files.small, "image/webp");
|
|
98
|
+
urls.medium = await uploadFile("avatar", `${currentUser.id}_64_${version}.webp`, files.medium, "image/webp");
|
|
99
|
+
urls.large = await uploadFile("avatar", `${currentUser.id}_128_${version}.webp`, files.large, "image/webp");
|
|
100
|
+
// Update user metadata
|
|
101
|
+
await supabaseBrowserClient.auth.updateUser({
|
|
102
|
+
data: {
|
|
103
|
+
avatar: `avatar/${currentUser.id}_128_${version}.webp`,
|
|
104
|
+
avatar_sizes: {
|
|
105
|
+
small: `avatar/${currentUser.id}_32_${version}.webp`,
|
|
106
|
+
medium: `avatar/${currentUser.id}_64_${version}.webp`,
|
|
107
|
+
large: `avatar/${currentUser.id}_128_${version}.webp`,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
// Update profile avatar_url
|
|
112
|
+
setProfile((prev) => ({ ...prev, avatar_url: urls.large }));
|
|
113
|
+
return urls;
|
|
114
|
+
};
|
|
115
|
+
const handleAvatarDelete = async () => {
|
|
116
|
+
if (!currentUser)
|
|
117
|
+
throw new Error("User not authenticated");
|
|
118
|
+
// Delete old files
|
|
119
|
+
await deleteFilesWithPrefix("avatar", currentUser.id);
|
|
120
|
+
// Update user metadata
|
|
121
|
+
await supabaseBrowserClient.auth.updateUser({
|
|
122
|
+
data: {
|
|
123
|
+
avatar: null,
|
|
124
|
+
avatar_sizes: {
|
|
125
|
+
small: null,
|
|
126
|
+
medium: null,
|
|
127
|
+
large: null,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
// Update profile
|
|
132
|
+
setProfile((prev) => ({ ...prev, avatar_url: "" }));
|
|
133
|
+
};
|
|
134
|
+
if (isLoading) {
|
|
135
|
+
return (_jsx("div", { className: "flex justify-center items-center min-h-[400px]", children: _jsx(Spinner, { size: "lg", label: "Loading profile..." }) }));
|
|
136
|
+
}
|
|
137
|
+
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(User, { className: "w-8 h-8" }), _jsx("h1", { className: "text-3xl font-bold", children: "Edit Profile" })] }), _jsx("form", { onSubmit: handleSubmit, children: _jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Photo de profil" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsx("div", { className: "flex justify-center", children: _jsx(AvatarUploader, { userId: currentUser?.id, bucket: "avatar", shape: "circle", onUpload: handleAvatarUpload, onDelete: handleAvatarDelete, initialAvatarPath: currentUser?.user_metadata?.avatar ||
|
|
138
|
+
profile.avatar_url ||
|
|
139
|
+
null, initialAvatarSizes: (() => {
|
|
140
|
+
const sizes = currentUser?.user_metadata
|
|
141
|
+
?.avatar_sizes;
|
|
142
|
+
if (!sizes)
|
|
143
|
+
return null;
|
|
144
|
+
return {
|
|
145
|
+
small: sizes.small ?? null,
|
|
146
|
+
medium: sizes.medium ?? null,
|
|
147
|
+
large: sizes.large ?? null,
|
|
148
|
+
};
|
|
149
|
+
})(), onUploaded: (urls) => {
|
|
150
|
+
setProfile((prev) => ({ ...prev, avatar_url: urls.large }));
|
|
151
|
+
}, onDeleted: () => {
|
|
152
|
+
setProfile((prev) => ({ ...prev, avatar_url: "" }));
|
|
153
|
+
} }) }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Personal Information" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: "First Name", placeholder: "Enter your first name", value: profile.first_name || "", onChange: (e) => handleChange("first_name", e.target.value) }), _jsx(Input, { label: "Last Name", placeholder: "Enter your last name", value: profile.last_name || "", onChange: (e) => handleChange("last_name", e.target.value) }), _jsx(Input, { label: "Phone", placeholder: "Enter your phone number", type: "tel", value: profile.phone || "", onChange: (e) => handleChange("phone", e.target.value), className: "md:col-span-2" }), _jsx(Textarea, { label: "Bio", placeholder: "Tell us about yourself", value: profile.bio || "", onChange: (e) => handleChange("bio", e.target.value), minRows: 3, className: "md:col-span-2" })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Professional Information" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: "Company", placeholder: "Enter your company name", value: profile.company || "", onChange: (e) => handleChange("company", e.target.value) }), _jsx(Input, { label: "Website", placeholder: "https://example.com", type: "url", value: profile.website || "", onChange: (e) => handleChange("website", e.target.value) }), _jsx(Input, { label: "Location", placeholder: "City, Country", value: profile.location || "", onChange: (e) => handleChange("location", e.target.value), className: "md:col-span-2" })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Preferences" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: "Language", placeholder: "en, fr, es...", value: profile.language || "", onChange: (e) => handleChange("language", e.target.value) }), _jsx(Input, { label: "Timezone", placeholder: "Europe/Paris, America/New_York...", value: profile.timezone || "", onChange: (e) => handleChange("timezone", e.target.value) })] }) })] }), _jsxs("div", { className: "flex justify-end gap-3", children: [_jsx(Button, { type: "button", variant: "flat", onPress: () => fetchProfile(), isDisabled: isSaving, children: "Cancel" }), _jsx(Button, { type: "submit", color: "primary", isLoading: isSaving, startContent: !isSaving && _jsx(Save, { className: "w-4 h-4" }), children: isSaving ? "Saving..." : "Save Changes" })] })] }) })] }));
|
|
4
154
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reglage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/reglage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reglage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/reglage.tsx"],"names":[],"mappings":"AAgCA,wBAAgB,WAAW,4CA+Q1B"}
|