@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.
Files changed (48) hide show
  1. package/README.md +533 -0
  2. package/dist/api/admin/users.d.ts +9 -0
  3. package/dist/api/admin/users.d.ts.map +1 -0
  4. package/dist/api/admin/users.js +38 -0
  5. package/dist/api/auth/me.d.ts +17 -0
  6. package/dist/api/auth/me.d.ts.map +1 -0
  7. package/dist/api/auth/me.js +32 -0
  8. package/dist/api/auth/profile.d.ts +32 -0
  9. package/dist/api/auth/profile.d.ts.map +1 -0
  10. package/dist/api/auth/profile.js +104 -0
  11. package/dist/api/public/signin.js +3 -3
  12. package/dist/api/storage.d.ts +13 -0
  13. package/dist/api/storage.d.ts.map +1 -0
  14. package/dist/api/storage.js +47 -0
  15. package/dist/auth.build.config.d.ts.map +1 -1
  16. package/dist/auth.build.config.js +42 -2
  17. package/dist/web/admin/users.d.ts.map +1 -1
  18. package/dist/web/admin/users.js +94 -2
  19. package/dist/web/auth/dashboard.d.ts +1 -1
  20. package/dist/web/auth/dashboard.d.ts.map +1 -1
  21. package/dist/web/auth/dashboard.js +42 -2
  22. package/dist/web/auth/profile.d.ts.map +1 -1
  23. package/dist/web/auth/profile.js +191 -2
  24. package/dist/web/auth/reglage.d.ts.map +1 -1
  25. package/dist/web/auth/reglage.js +98 -2
  26. package/dist/web/public/SignInPage.d.ts.map +1 -1
  27. package/dist/web/public/SignInPage.js +1 -1
  28. package/dist/web/public/SignUpPage.js +1 -1
  29. package/package.json +8 -7
  30. package/src/api/admin/users.ts +51 -0
  31. package/src/api/auth/me.ts +39 -0
  32. package/src/api/auth/profile.ts +142 -0
  33. package/src/api/public/signin.ts +3 -3
  34. package/src/api/storage.ts +66 -0
  35. package/src/auth.build.config.ts +42 -2
  36. package/src/web/admin/users.tsx +290 -1
  37. package/src/web/auth/dashboard.tsx +207 -1
  38. package/src/web/auth/profile.tsx +420 -1
  39. package/src/web/auth/reglage.tsx +284 -1
  40. package/src/web/public/SignInPage.tsx +1 -2
  41. package/src/web/public/SignUpPage.tsx +2 -2
  42. package/supabase/.temp/cli-latest +1 -0
  43. package/supabase/migrations/20251112000000_user_init.sql +1 -1
  44. package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +206 -0
  45. package/supabase/migrations/20251112000002_sync_avatars.sql +54 -0
  46. package/supabase/migrations-down/20251112000000_user_init.down.sql +2 -0
  47. package/supabase/migrations-down/20251112000001_auto_profile_and_admin_view.down.sql +23 -0
  48. package/supabase/migrations-down/20251112000002_sync_avatars.down.sql +9 -0
@@ -0,0 +1,104 @@
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 }, } = await supabase.auth.getUser();
11
+ // L'utilisateur est déjà authentifié grâce au middleware
12
+ const { data: profile, error: profileError } = await supabase
13
+ .from("user_profil")
14
+ .select("*")
15
+ .eq("owner_id", user.id)
16
+ .single();
17
+ if (profileError && profileError.code !== "PGRST116") {
18
+ // PGRST116 = no rows returned, which is OK
19
+ return NextResponse.json({ error: "Database Error", message: profileError.message }, { status: 500 });
20
+ }
21
+ return NextResponse.json({ data: profile || null });
22
+ }
23
+ catch (error) {
24
+ console.error("Error fetching profile:", error);
25
+ return NextResponse.json({ error: "Internal Server Error", message: "Failed to fetch profile" }, { status: 500 });
26
+ }
27
+ }
28
+ /**
29
+ * PUT /api/auth/profile
30
+ * Updates the user's profile
31
+ */
32
+ export async function PUT(request) {
33
+ try {
34
+ const supabase = await getSupabaseServerClient();
35
+ const { data: { user }, } = await supabase.auth.getUser();
36
+ // L'utilisateur est déjà authentifié grâce au middleware
37
+ const body = await request.json();
38
+ const { first_name, last_name, avatar_url, bio, phone, company, website, location, language, timezone, preferences, } = body;
39
+ // Check if profile exists
40
+ const { data: existingProfile } = await supabase
41
+ .from("user_profil")
42
+ .select("id")
43
+ .eq("owner_id", user.id)
44
+ .single();
45
+ let result;
46
+ if (existingProfile) {
47
+ // Update existing profile
48
+ result = await supabase
49
+ .from("user_profil")
50
+ .update({
51
+ first_name,
52
+ last_name,
53
+ avatar_url,
54
+ bio,
55
+ phone,
56
+ company,
57
+ website,
58
+ location,
59
+ language,
60
+ timezone,
61
+ preferences,
62
+ })
63
+ .eq("owner_id", user.id)
64
+ .select()
65
+ .single();
66
+ }
67
+ else {
68
+ // Create new profile
69
+ result = await supabase
70
+ .from("user_profil")
71
+ .insert({
72
+ owner_id: user.id,
73
+ first_name,
74
+ last_name,
75
+ avatar_url,
76
+ bio,
77
+ phone,
78
+ company,
79
+ website,
80
+ location,
81
+ language,
82
+ timezone,
83
+ preferences,
84
+ })
85
+ .select()
86
+ .single();
87
+ }
88
+ if (result.error) {
89
+ return NextResponse.json({ error: "Database Error", message: result.error.message }, { status: 500 });
90
+ }
91
+ return NextResponse.json({ data: result.data });
92
+ }
93
+ catch (error) {
94
+ console.error("Error updating profile:", error);
95
+ return NextResponse.json({ error: "Internal Server Error", message: "Failed to update profile" }, { status: 500 });
96
+ }
97
+ }
98
+ /**
99
+ * PATCH /api/auth/profile
100
+ * Partially updates the user's profile
101
+ */
102
+ export async function PATCH(request) {
103
+ return PUT(request);
104
+ }
@@ -2,9 +2,9 @@ import { getSupabaseServerClient } from "@lastbrain/core/server";
2
2
  const jsonResponse = (payload, status = 200) => {
3
3
  return new Response(JSON.stringify(payload), {
4
4
  headers: {
5
- "content-type": "application/json"
5
+ "content-type": "application/json",
6
6
  },
7
- status
7
+ status,
8
8
  });
9
9
  };
10
10
  export async function POST(request) {
@@ -16,7 +16,7 @@ export async function POST(request) {
16
16
  }
17
17
  const { error, data } = await supabase.auth.signInWithPassword({
18
18
  email,
19
- password
19
+ password,
20
20
  });
21
21
  if (error) {
22
22
  return jsonResponse({ error: error.message }, 400);
@@ -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,CAC/B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,IAAI,CAAC,CAQf;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,iBAuJtB,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -45,12 +45,52 @@ 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
+ },
69
+ {
70
+ method: "GET",
71
+ path: "/api/auth/me",
72
+ handlerExport: "GET",
73
+ entryPoint: "api/auth/me",
74
+ authRequired: true,
75
+ },
76
+ {
77
+ method: "GET",
78
+ path: "/api/admin/users",
79
+ handlerExport: "GET",
80
+ entryPoint: "api/admin/users",
81
+ authRequired: true,
82
+ },
48
83
  ],
49
84
  migrations: {
50
85
  enabled: true,
51
86
  priority: 20,
52
87
  path: "supabase/migrations",
53
- files: ["001_auth_base.sql"],
88
+ files: [
89
+ "20251112000000_user_init.sql",
90
+ "20251112000001_auto_profile_and_admin_view.sql",
91
+ "20251112000002_sync_avatars.sql"
92
+ ],
93
+ migrationsDownPath: "supabase/migrations-down",
54
94
  },
55
95
  menu: {
56
96
  public: [
@@ -74,7 +114,7 @@ const authBuildConfig = {
74
114
  title: "Gestion des utilisateurs",
75
115
  description: "Gérez les utilisateurs de la plateforme",
76
116
  icon: "Users2",
77
- path: "/admin/users",
117
+ path: "/admin/auth/users",
78
118
  order: 1,
79
119
  },
80
120
  ],
@@ -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":"AAsDA,wBAAgB,cAAc,4CA6O7B"}
@@ -1,4 +1,96 @@
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 { useCallback, 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
+ const fetchUsers = useCallback(async () => {
18
+ try {
19
+ setIsLoading(true);
20
+ const params = new URLSearchParams({
21
+ page: pagination.page.toString(),
22
+ per_page: pagination.per_page.toString(),
23
+ });
24
+ if (searchQuery) {
25
+ params.append("search", searchQuery);
26
+ }
27
+ const response = await fetch(`/api/admin/users?${params}`);
28
+ if (response.status === 403) {
29
+ setError("You don't have permission to access this page");
30
+ addToast({
31
+ title: "Access Denied",
32
+ description: "Superadmin access required",
33
+ color: "danger",
34
+ });
35
+ return;
36
+ }
37
+ if (!response.ok) {
38
+ throw new Error("Failed to fetch users");
39
+ }
40
+ const result = await response.json();
41
+ setUsers(result.data || []);
42
+ if (result.pagination) {
43
+ setPagination(result.pagination);
44
+ }
45
+ setError(null);
46
+ }
47
+ catch (err) {
48
+ setError(err instanceof Error ? err.message : "An error occurred");
49
+ addToast({
50
+ title: "Error",
51
+ description: "Failed to load users",
52
+ color: "danger",
53
+ });
54
+ }
55
+ finally {
56
+ setIsLoading(false);
57
+ }
58
+ }, [pagination.page, pagination.per_page, searchQuery]);
59
+ useEffect(() => {
60
+ fetchUsers();
61
+ }, [fetchUsers]);
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: "ROLE" }), _jsx(TableColumn, { children: "COMPANY" }), _jsx(TableColumn, { children: "LAST SIGN IN" }), _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
+ : user.full_name || "N/A";
87
+ const roleColor = user.role === "admin" || user.role === "superadmin"
88
+ ? "danger"
89
+ : user.role === "moderator"
90
+ ? "secondary"
91
+ : "default";
92
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Avatar, { src: `/api/storage/${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(Chip, { color: roleColor, size: "sm", variant: "flat", children: user.role || "user" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.profile?.company || "-" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-small", children: user.last_sign_in_at
93
+ ? formatDate(user.last_sign_in_at)
94
+ : "Never" }) }), _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));
95
+ }) })] }), 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
96
  }
@@ -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,mDAoL5B"}
@@ -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: _jsxs(Spinner, { color: "primary", size: "lg", children: [" ", _jsx("span", { className: "text-xs text-default-700", children: "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,4CA4X1B"}
@@ -1,4 +1,193 @@
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 in database
112
+ try {
113
+ const response = await fetch("/api/auth/profile", {
114
+ method: "PATCH",
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ },
118
+ body: JSON.stringify({
119
+ avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
120
+ }),
121
+ });
122
+ if (!response.ok) {
123
+ console.error("Failed to update avatar_url in profile");
124
+ }
125
+ }
126
+ catch (error) {
127
+ console.error("Error updating profile avatar_url:", error);
128
+ }
129
+ // Update profile avatar_url locally
130
+ setProfile((prev) => ({
131
+ ...prev,
132
+ avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
133
+ }));
134
+ return urls;
135
+ };
136
+ const handleAvatarDelete = async () => {
137
+ if (!currentUser)
138
+ throw new Error("User not authenticated");
139
+ // Delete old files
140
+ await deleteFilesWithPrefix("avatar", currentUser.id);
141
+ // Update user metadata
142
+ await supabaseBrowserClient.auth.updateUser({
143
+ data: {
144
+ avatar: null,
145
+ avatar_sizes: {
146
+ small: null,
147
+ medium: null,
148
+ large: null,
149
+ },
150
+ },
151
+ });
152
+ // Update profile avatar_url in database
153
+ try {
154
+ const response = await fetch("/api/auth/profile", {
155
+ method: "PATCH",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ },
159
+ body: JSON.stringify({
160
+ avatar_url: null,
161
+ }),
162
+ });
163
+ if (!response.ok) {
164
+ console.error("Failed to update avatar_url in profile");
165
+ }
166
+ }
167
+ catch (error) {
168
+ console.error("Error updating profile avatar_url:", error);
169
+ }
170
+ // Update profile locally
171
+ setProfile((prev) => ({ ...prev, avatar_url: "" }));
172
+ };
173
+ if (isLoading) {
174
+ return (_jsx("div", { className: "flex justify-center items-center min-h-[400px]", children: _jsx(Spinner, { size: "lg", label: "Loading profile..." }) }));
175
+ }
176
+ 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 ||
177
+ profile.avatar_url ||
178
+ null, initialAvatarSizes: (() => {
179
+ const sizes = currentUser?.user_metadata
180
+ ?.avatar_sizes;
181
+ if (!sizes)
182
+ return null;
183
+ return {
184
+ small: sizes.small ?? null,
185
+ medium: sizes.medium ?? null,
186
+ large: sizes.large ?? null,
187
+ };
188
+ })(), onUploaded: (urls) => {
189
+ setProfile((prev) => ({ ...prev, avatar_url: urls.large }));
190
+ }, onDeleted: () => {
191
+ setProfile((prev) => ({ ...prev, avatar_url: "" }));
192
+ } }) }) })] }), _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
193
  }
@@ -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,4CA6P1B"}