feat: add group binding management

Signed-off-by: d0zingcat <iamtangli42@gmail.com>
This commit is contained in:
2026-01-16 21:00:14 +08:00
parent 2f971290ce
commit 1403baaeb6
11 changed files with 420 additions and 35 deletions

View File

@@ -7,6 +7,7 @@ interface GroupBinding {
id: string;
chatId: string;
name: string;
status: "pending" | "approved" | "rejected";
}
interface KnownGroup {
@@ -103,7 +104,14 @@ export default function GroupBindingsModal({
);
if (res.ok) {
setStatus({ type: "success", message: "Group bound successfully!" });
const data = (await res.json()) as GroupBinding;
setStatus({
type: "success",
message:
data.status === "approved"
? "Group bound successfully!"
: "Request submitted! Waiting for approval.",
});
fetchBindings();
setSelectedChatId("");
} else {
@@ -166,11 +174,21 @@ export default function GroupBindingsModal({
key={binding.id}
className="flex justify-between items-center p-3"
>
<div className="flex items-center">
<div className="flex items-center flex-1">
<MessageCircle className="w-4 h-4 text-gray-400 mr-2" />
<span className="text-sm text-gray-700">
<span className="text-sm text-gray-700 mr-2">
{binding.name}
</span>
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium ${binding.status === "approved"
? "bg-green-100 text-green-800"
: binding.status === "rejected"
? "bg-red-100 text-red-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{binding.status}
</span>
</div>
<button
type="button"

View File

@@ -30,38 +30,45 @@ export default function AdminView() {
</div>
<div className="bg-white shadow rounded-lg p-6">
<div className="border-b border-gray-200 mb-6">
<div className="border-b border-gray-200 mb-6 overflow-x-auto">
<nav className="-mb-px flex space-x-8">
<button
type="button"
onClick={() => setActiveTab("load")}
className={`${
activeTab === "load"
className={`${activeTab === "load"
? "border-indigo-500 text-indigo-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
>
System Load
</button>
<button
type="button"
onClick={() => setActiveTab("requests")}
className={`${
activeTab === "requests"
className={`${activeTab === "requests"
? "border-indigo-500 text-indigo-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
>
Topic Requests
</button>
<button
type="button"
onClick={() => setActiveTab("topics")}
className={`${
activeTab === "topics"
onClick={() => setActiveTab("group-requests")}
className={`${activeTab === "group-requests"
? "border-indigo-500 text-indigo-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
>
Group Bindings
</button>
<button
type="button"
onClick={() => setActiveTab("topics")}
className={`${activeTab === "topics"
? "border-indigo-500 text-indigo-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
} whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm`}
>
All Topics
</button>
@@ -70,12 +77,126 @@ export default function AdminView() {
{activeTab === "load" && <SystemLoadView />}
{activeTab === "requests" && <TopicRequestsList />}
{activeTab === "group-requests" && <GroupRequestsList />}
{activeTab === "topics" && <TopicsManagement />}
</div>
</div>
);
}
interface GroupRequest {
id: string;
topicId: string;
chatId: string;
name: string;
status: string;
createdAt: string;
topic?: Topic;
creator?: TopicUser;
}
function GroupRequestsList() {
const [requests, setRequests] = useState<GroupRequest[]>([]);
const [loading, setLoading] = useState(true);
const fetchRequests = useCallback(async () => {
setLoading(true);
try {
// @ts-ignore - groups requests might not be in the generated client yet
const res = await client.api.topics.groups.requests.$get(undefined, {
init: { credentials: "include" },
});
if (res.ok) {
const data = await res.json();
if (Array.isArray(data)) {
setRequests(data as unknown as GroupRequest[]);
} else {
setRequests([]);
}
} else {
setRequests([]);
}
} catch (error) {
console.error(error);
setRequests([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchRequests();
}, [fetchRequests]);
const handleAction = async (
req: GroupRequest,
action: "approve" | "reject",
) => {
try {
// @ts-ignore
await client.api.topics[":id"].groups[":bindingId"][action].$post(
{ param: { id: req.topicId, bindingId: req.id } },
{ init: { credentials: "include" } },
);
fetchRequests();
} catch (error) {
console.error(error);
}
};
if (loading) return <div>Loading group requests...</div>;
if (requests.length === 0) {
return (
<div className="text-center py-8 text-gray-500">
No pending group binding requests.
</div>
);
}
return (
<div className="overflow-hidden">
<ul className="divide-y divide-gray-200">
{requests.map((req) => (
<li key={req.id} className="py-4 flex justify-between items-center">
<div>
<p className="font-medium text-gray-900">
Group: <span className="text-indigo-600">{req.name}</span>
</p>
<p className="text-sm text-gray-500">
Topic: <span className="font-semibold">{req.topic?.name}</span> (
{req.topic?.slug})
</p>
<p className="text-sm text-gray-500">
Requested by: {req.creator?.name || "Unknown"}
</p>
<p className="text-xs text-gray-400 mt-1">
ID: {req.chatId}
</p>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => handleAction(req, "approve")}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm font-medium shadow-sm transition-colors"
>
Approve
</button>
<button
type="button"
onClick={() => handleAction(req, "reject")}
className="px-4 py-2 bg-red-100 text-red-700 rounded hover:bg-red-200 text-sm font-medium transition-colors"
>
Reject
</button>
</div>
</li>
))}
</ul>
</div>
);
}
function TopicsManagement() {
const [topics, setTopics] = useState<Topic[]>([]);
const [loading, setLoading] = useState(true);
@@ -170,13 +291,12 @@ function TopicsManagement() {
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
topic.status === "approved"
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${topic.status === "approved"
? "bg-green-100 text-green-800"
: topic.status === "rejected"
? "bg-red-100 text-red-800"
: "bg-yellow-100 text-yellow-800"
}`}
}`}
>
{topic.status}
</span>

View File

@@ -10,6 +10,8 @@ interface User {
feishuUserId?: string;
email?: string;
personalToken?: string;
isTrusted?: boolean;
isAdmin?: boolean;
}
export default function UsersView() {
@@ -85,6 +87,28 @@ export default function UsersView() {
}
};
const handleToggleTrusted = async (user: User) => {
try {
await client.api.users[":id"].$put(
{
param: { id: user.id },
json: {
name: user.name,
feishuUserId: user.feishuUserId,
email: user.email,
isTrusted: !user.isTrusted,
},
},
{
init: { credentials: "include" },
},
);
fetchUsers();
} catch (error) {
console.error("Error toggling trusted status:", error);
}
};
if (loading) return <div className="p-4">Loading...</div>;
return (
@@ -110,9 +134,16 @@ export default function UsersView() {
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-indigo-600 truncate">
{user.name}
</p>
<div className="flex items-center space-x-2">
<p className="text-sm font-medium text-indigo-600 truncate">
{user.name}
</p>
{user.isAdmin && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">
Admin
</span>
)}
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="sm:flex flex-col">
<p className="flex items-center text-sm text-gray-500">
@@ -134,11 +165,25 @@ export default function UsersView() {
</div>
</div>
{currentUser?.isAdmin && (
<div className="ml-4 flex items-center space-x-2">
<div className="ml-4 flex items-center space-x-4">
<div className="flex items-center">
<label className="inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={user.isTrusted || false}
onChange={() => handleToggleTrusted(user)}
/>
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600" />
<span className="ms-3 text-sm font-medium text-gray-500">
Trusted
</span>
</label>
</div>
<button
type="button"
onClick={() => handleDelete(user.id)}
className="text-red-600 hover:text-red-900 p-2"
className="text-red-400 hover:text-red-600 p-2 transition-colors"
>
<Trash2 className="w-5 h-5" />
</button>