@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
package/src/web/admin/users.tsx
CHANGED
|
@@ -1,3 +1,266 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardBody,
|
|
7
|
+
CardHeader,
|
|
8
|
+
Table,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableColumn,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableRow,
|
|
13
|
+
TableCell,
|
|
14
|
+
Spinner,
|
|
15
|
+
Chip,
|
|
16
|
+
Input,
|
|
17
|
+
Button,
|
|
18
|
+
Pagination,
|
|
19
|
+
Avatar,
|
|
20
|
+
addToast,
|
|
21
|
+
} from "@lastbrain/ui";
|
|
22
|
+
import { Users, Search, RefreshCw } from "lucide-react";
|
|
23
|
+
|
|
24
|
+
interface User {
|
|
25
|
+
id: string;
|
|
26
|
+
email: string;
|
|
27
|
+
created_at: string;
|
|
28
|
+
profile: {
|
|
29
|
+
first_name?: string;
|
|
30
|
+
last_name?: string;
|
|
31
|
+
avatar_url?: string;
|
|
32
|
+
company?: string;
|
|
33
|
+
location?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PaginationData {
|
|
38
|
+
page: number;
|
|
39
|
+
per_page: number;
|
|
40
|
+
total: number;
|
|
41
|
+
total_pages: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
1
44
|
export function AdminUsersPage() {
|
|
2
|
-
|
|
45
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
46
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
49
|
+
const [pagination, setPagination] = useState<PaginationData>({
|
|
50
|
+
page: 1,
|
|
51
|
+
per_page: 20,
|
|
52
|
+
total: 0,
|
|
53
|
+
total_pages: 0,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
fetchUsers();
|
|
58
|
+
}, [pagination.page]);
|
|
59
|
+
|
|
60
|
+
const fetchUsers = async () => {
|
|
61
|
+
try {
|
|
62
|
+
setIsLoading(true);
|
|
63
|
+
const params = new URLSearchParams({
|
|
64
|
+
page: pagination.page.toString(),
|
|
65
|
+
per_page: pagination.per_page.toString(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (searchQuery) {
|
|
69
|
+
params.append("search", searchQuery);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const response = await fetch(`/api/admin/users?${params}`);
|
|
73
|
+
|
|
74
|
+
if (response.status === 403) {
|
|
75
|
+
setError("You don't have permission to access this page");
|
|
76
|
+
addToast({
|
|
77
|
+
title: "Access Denied",
|
|
78
|
+
description: "Superadmin access required",
|
|
79
|
+
color: "danger",
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error("Failed to fetch users");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = await response.json();
|
|
89
|
+
setUsers(result.data || []);
|
|
90
|
+
if (result.pagination) {
|
|
91
|
+
setPagination(result.pagination);
|
|
92
|
+
}
|
|
93
|
+
setError(null);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
96
|
+
addToast({
|
|
97
|
+
title: "Error",
|
|
98
|
+
description: "Failed to load users",
|
|
99
|
+
color: "danger",
|
|
100
|
+
});
|
|
101
|
+
} finally {
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleSearch = () => {
|
|
107
|
+
setPagination((prev) => ({ ...prev, page: 1 }));
|
|
108
|
+
fetchUsers();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handlePageChange = (page: number) => {
|
|
112
|
+
setPagination((prev) => ({ ...prev, page }));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const formatDate = (dateString: string) => {
|
|
116
|
+
return new Date(dateString).toLocaleDateString("en-US", {
|
|
117
|
+
year: "numeric",
|
|
118
|
+
month: "short",
|
|
119
|
+
day: "numeric",
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (error && users.length === 0) {
|
|
124
|
+
return (
|
|
125
|
+
<div className="pt-12 pb-12 max-w-7xl mx-auto px-4">
|
|
126
|
+
<Card>
|
|
127
|
+
<CardBody>
|
|
128
|
+
<p className="text-danger">{error}</p>
|
|
129
|
+
</CardBody>
|
|
130
|
+
</Card>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="pt-12 pb-12 max-w-7xl mx-auto px-4">
|
|
137
|
+
<div className="flex items-center gap-2 mb-8">
|
|
138
|
+
<Users className="w-8 h-8" />
|
|
139
|
+
<h1 className="text-3xl font-bold">User Management</h1>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<Card>
|
|
143
|
+
<CardHeader>
|
|
144
|
+
<div className="flex flex-col md:flex-row gap-4 w-full">
|
|
145
|
+
<div className="flex gap-2 flex-1">
|
|
146
|
+
<Input
|
|
147
|
+
placeholder="Search by email or name..."
|
|
148
|
+
value={searchQuery}
|
|
149
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
150
|
+
onKeyPress={(e) => {
|
|
151
|
+
if (e.key === "Enter") {
|
|
152
|
+
handleSearch();
|
|
153
|
+
}
|
|
154
|
+
}}
|
|
155
|
+
startContent={<Search className="w-4 h-4 text-default-400" />}
|
|
156
|
+
className="flex-1"
|
|
157
|
+
/>
|
|
158
|
+
<Button
|
|
159
|
+
color="primary"
|
|
160
|
+
onPress={handleSearch}
|
|
161
|
+
isDisabled={isLoading}
|
|
162
|
+
>
|
|
163
|
+
Search
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
<Button
|
|
167
|
+
variant="flat"
|
|
168
|
+
onPress={fetchUsers}
|
|
169
|
+
isDisabled={isLoading}
|
|
170
|
+
startContent={<RefreshCw className="w-4 h-4" />}
|
|
171
|
+
>
|
|
172
|
+
Refresh
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</CardHeader>
|
|
176
|
+
<CardBody>
|
|
177
|
+
{isLoading ? (
|
|
178
|
+
<div className="flex justify-center items-center py-12">
|
|
179
|
+
<Spinner size="lg" label="Loading users..." />
|
|
180
|
+
</div>
|
|
181
|
+
) : users.length === 0 ? (
|
|
182
|
+
<div className="text-center py-12 text-default-500">
|
|
183
|
+
No users found
|
|
184
|
+
</div>
|
|
185
|
+
) : (
|
|
186
|
+
<>
|
|
187
|
+
<Table aria-label="Users table">
|
|
188
|
+
<TableHeader>
|
|
189
|
+
<TableColumn>USER</TableColumn>
|
|
190
|
+
<TableColumn>EMAIL</TableColumn>
|
|
191
|
+
<TableColumn>COMPANY</TableColumn>
|
|
192
|
+
<TableColumn>LOCATION</TableColumn>
|
|
193
|
+
<TableColumn>CREATED</TableColumn>
|
|
194
|
+
<TableColumn>STATUS</TableColumn>
|
|
195
|
+
</TableHeader>
|
|
196
|
+
<TableBody>
|
|
197
|
+
{users.map((user) => {
|
|
198
|
+
const fullName =
|
|
199
|
+
user.profile?.first_name && user.profile?.last_name
|
|
200
|
+
? `${user.profile.first_name} ${user.profile.last_name}`
|
|
201
|
+
: "N/A";
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<TableRow key={user.id}>
|
|
205
|
+
<TableCell>
|
|
206
|
+
<div className="flex items-center gap-2">
|
|
207
|
+
<Avatar
|
|
208
|
+
src={user.profile?.avatar_url}
|
|
209
|
+
name={fullName}
|
|
210
|
+
size="sm"
|
|
211
|
+
/>
|
|
212
|
+
<span className="text-small font-medium">
|
|
213
|
+
{fullName}
|
|
214
|
+
</span>
|
|
215
|
+
</div>
|
|
216
|
+
</TableCell>
|
|
217
|
+
<TableCell>
|
|
218
|
+
<span className="text-small">{user.email}</span>
|
|
219
|
+
</TableCell>
|
|
220
|
+
<TableCell>
|
|
221
|
+
<span className="text-small">
|
|
222
|
+
{user.profile?.company || "-"}
|
|
223
|
+
</span>
|
|
224
|
+
</TableCell>
|
|
225
|
+
<TableCell>
|
|
226
|
+
<span className="text-small">
|
|
227
|
+
{user.profile?.location || "-"}
|
|
228
|
+
</span>
|
|
229
|
+
</TableCell>
|
|
230
|
+
<TableCell>
|
|
231
|
+
<span className="text-small">
|
|
232
|
+
{formatDate(user.created_at)}
|
|
233
|
+
</span>
|
|
234
|
+
</TableCell>
|
|
235
|
+
<TableCell>
|
|
236
|
+
<Chip color="success" size="sm" variant="flat">
|
|
237
|
+
Active
|
|
238
|
+
</Chip>
|
|
239
|
+
</TableCell>
|
|
240
|
+
</TableRow>
|
|
241
|
+
);
|
|
242
|
+
})}
|
|
243
|
+
</TableBody>
|
|
244
|
+
</Table>
|
|
245
|
+
|
|
246
|
+
{pagination.total_pages > 1 && (
|
|
247
|
+
<div className="flex justify-center mt-4">
|
|
248
|
+
<Pagination
|
|
249
|
+
total={pagination.total_pages}
|
|
250
|
+
page={pagination.page}
|
|
251
|
+
onChange={handlePageChange}
|
|
252
|
+
showControls
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
<div className="mt-4 text-small text-default-500 text-center">
|
|
258
|
+
Showing {users.length} of {pagination.total} users
|
|
259
|
+
</div>
|
|
260
|
+
</>
|
|
261
|
+
)}
|
|
262
|
+
</CardBody>
|
|
263
|
+
</Card>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
3
266
|
}
|
|
@@ -1,3 +1,202 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardBody,
|
|
7
|
+
CardHeader,
|
|
8
|
+
Spinner,
|
|
9
|
+
Chip,
|
|
10
|
+
Divider,
|
|
11
|
+
Avatar,
|
|
12
|
+
} from "@lastbrain/ui";
|
|
13
|
+
import { User, Mail, Calendar, Shield } from "lucide-react";
|
|
14
|
+
|
|
15
|
+
interface UserData {
|
|
16
|
+
id: string;
|
|
17
|
+
email: string;
|
|
18
|
+
created_at: string;
|
|
19
|
+
profile: {
|
|
20
|
+
first_name?: string;
|
|
21
|
+
last_name?: string;
|
|
22
|
+
avatar_url?: string;
|
|
23
|
+
bio?: string;
|
|
24
|
+
company?: string;
|
|
25
|
+
location?: string;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
1
29
|
export function DashboardPage() {
|
|
2
|
-
|
|
30
|
+
const [userData, setUserData] = useState<UserData | null>(null);
|
|
31
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
32
|
+
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
fetchUserData();
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const fetchUserData = async () => {
|
|
39
|
+
try {
|
|
40
|
+
setIsLoading(true);
|
|
41
|
+
const response = await fetch("/api/auth/me");
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error("Failed to fetch user data");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await response.json();
|
|
48
|
+
setUserData(result.data);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
51
|
+
} finally {
|
|
52
|
+
setIsLoading(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (isLoading) {
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex justify-center items-center min-h-[400px]">
|
|
59
|
+
<Spinner size="lg" label="Loading dashboard..." />
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (error) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="pt-12">
|
|
67
|
+
<Card className="max-w-2xl mx-auto">
|
|
68
|
+
<CardBody>
|
|
69
|
+
<p className="text-danger">Error: {error}</p>
|
|
70
|
+
</CardBody>
|
|
71
|
+
</Card>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!userData) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fullName = userData.profile?.first_name && userData.profile?.last_name
|
|
81
|
+
? `${userData.profile.first_name} ${userData.profile.last_name}`
|
|
82
|
+
: "User";
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="pt-12 pb-12 max-w-6xl mx-auto px-4">
|
|
86
|
+
<h1 className="text-3xl font-bold mb-8">Dashboard</h1>
|
|
87
|
+
|
|
88
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
89
|
+
{/* Profile Summary Card */}
|
|
90
|
+
<Card className="col-span-full md:col-span-1">
|
|
91
|
+
<CardHeader className="flex gap-3">
|
|
92
|
+
<Avatar
|
|
93
|
+
src={userData.profile?.avatar_url}
|
|
94
|
+
icon={<User />}
|
|
95
|
+
size="lg"
|
|
96
|
+
className="flex-shrink-0"
|
|
97
|
+
/>
|
|
98
|
+
<div className="flex flex-col">
|
|
99
|
+
<p className="text-xl font-semibold">{fullName}</p>
|
|
100
|
+
<p className="text-small text-default-500">{userData.email}</p>
|
|
101
|
+
</div>
|
|
102
|
+
</CardHeader>
|
|
103
|
+
<Divider />
|
|
104
|
+
<CardBody>
|
|
105
|
+
<div className="space-y-3">
|
|
106
|
+
<div className="flex items-center gap-2">
|
|
107
|
+
<Mail className="w-4 h-4 text-default-400" />
|
|
108
|
+
<span className="text-small">{userData.email}</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex items-center gap-2">
|
|
111
|
+
<Calendar className="w-4 h-4 text-default-400" />
|
|
112
|
+
<span className="text-small">
|
|
113
|
+
Member since {new Date(userData.created_at).toLocaleDateString()}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
{userData.profile?.company && (
|
|
117
|
+
<div className="flex items-center gap-2">
|
|
118
|
+
<Shield className="w-4 h-4 text-default-400" />
|
|
119
|
+
<span className="text-small">{userData.profile.company}</span>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</CardBody>
|
|
124
|
+
</Card>
|
|
125
|
+
|
|
126
|
+
{/* Account Status Card */}
|
|
127
|
+
<Card>
|
|
128
|
+
<CardHeader>
|
|
129
|
+
<h3 className="text-lg font-semibold">Account Status</h3>
|
|
130
|
+
</CardHeader>
|
|
131
|
+
<Divider />
|
|
132
|
+
<CardBody>
|
|
133
|
+
<div className="space-y-4">
|
|
134
|
+
<div className="flex justify-between items-center">
|
|
135
|
+
<span className="text-small">Status</span>
|
|
136
|
+
<Chip color="success" size="sm" variant="flat">
|
|
137
|
+
Active
|
|
138
|
+
</Chip>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="flex justify-between items-center">
|
|
141
|
+
<span className="text-small">Profile</span>
|
|
142
|
+
<Chip
|
|
143
|
+
color={userData.profile ? "success" : "warning"}
|
|
144
|
+
size="sm"
|
|
145
|
+
variant="flat"
|
|
146
|
+
>
|
|
147
|
+
{userData.profile ? "Complete" : "Incomplete"}
|
|
148
|
+
</Chip>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</CardBody>
|
|
152
|
+
</Card>
|
|
153
|
+
|
|
154
|
+
{/* Bio Card (if available) */}
|
|
155
|
+
{userData.profile?.bio && (
|
|
156
|
+
<Card className="col-span-full">
|
|
157
|
+
<CardHeader>
|
|
158
|
+
<h3 className="text-lg font-semibold">Bio</h3>
|
|
159
|
+
</CardHeader>
|
|
160
|
+
<Divider />
|
|
161
|
+
<CardBody>
|
|
162
|
+
<p className="text-small text-default-600">{userData.profile.bio}</p>
|
|
163
|
+
</CardBody>
|
|
164
|
+
</Card>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{/* Quick Stats */}
|
|
168
|
+
<Card className="col-span-full">
|
|
169
|
+
<CardHeader>
|
|
170
|
+
<h3 className="text-lg font-semibold">Quick Stats</h3>
|
|
171
|
+
</CardHeader>
|
|
172
|
+
<Divider />
|
|
173
|
+
<CardBody>
|
|
174
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
175
|
+
<div className="text-center">
|
|
176
|
+
<p className="text-2xl font-bold text-primary">0</p>
|
|
177
|
+
<p className="text-small text-default-500">Projects</p>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="text-center">
|
|
180
|
+
<p className="text-2xl font-bold text-success">0</p>
|
|
181
|
+
<p className="text-small text-default-500">Tasks</p>
|
|
182
|
+
</div>
|
|
183
|
+
<div className="text-center">
|
|
184
|
+
<p className="text-2xl font-bold text-warning">0</p>
|
|
185
|
+
<p className="text-small text-default-500">Notifications</p>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="text-center">
|
|
188
|
+
<p className="text-2xl font-bold text-secondary">
|
|
189
|
+
{Math.floor(
|
|
190
|
+
(Date.now() - new Date(userData.created_at).getTime()) /
|
|
191
|
+
(1000 * 60 * 60 * 24)
|
|
192
|
+
)}
|
|
193
|
+
</p>
|
|
194
|
+
<p className="text-small text-default-500">Days active</p>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</CardBody>
|
|
198
|
+
</Card>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
3
202
|
}
|