@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.
@@ -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,iBA+GtB,CAAC;AAEF,eAAe,eAAe,CAAC"}
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":"AAAA,wBAAgB,cAAc,4CAE7B"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/web/admin/users.tsx"],"names":[],"mappings":"AA2CA,wBAAgB,cAAc,4CA8N7B"}
@@ -1,4 +1,89 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
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
- return _jsx("div", { children: "Admin Users Page" });
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":"AAAA,wBAAgB,aAAa,4CAE5B"}
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
- import { jsx as _jsx } from "react/jsx-runtime";
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
- return _jsx("div", { className: "pt-12", children: "Welcome to your dashboard!" });
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":"AAAA,wBAAgB,WAAW,4CAE1B"}
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/web/auth/profile.tsx"],"names":[],"mappings":"AAyCA,wBAAgB,WAAW,4CAmV1B"}
@@ -1,4 +1,154 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
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
- return _jsx("div", { className: "pt-12", children: "Welcome to your Profile!" });
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":"AAAA,wBAAgB,WAAW,4CAE1B"}
1
+ {"version":3,"file":"reglage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/reglage.tsx"],"names":[],"mappings":"AAgCA,wBAAgB,WAAW,4CA+Q1B"}