Files
solo-company-feed/app/admin/page.tsx

182 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { cookies } from "next/headers";
import { AdminPostList } from "@/components/AdminPostList";
import { AdminUserManager } from "@/components/AdminUserManager";
import { CreatePostForm } from "@/components/CreatePostForm";
import { cookieName, isAdminSession, verifySession } from "@/lib/auth";
import { getDb } from "@/lib/mongo";
import { buildOwnedPostFilter, serializePost } from "@/lib/posts";
import { findUserById, getEffectiveDailyPostLimit, getShanghaiDayRange } from "@/lib/users";
export const dynamic = "force-dynamic";
type ManagedUser = {
id: string;
username: string;
displayName: string;
role: "admin" | "user";
dailyPostLimit: number;
postCount: number;
todayPostCount: number;
posts: Array<{ slug: string; title: string; createdAt: string }>;
};
async function fetchRecentPosts(session: Awaited<ReturnType<typeof verifySession>>) {
const db = await getDb();
const posts = await db
.collection("posts")
.find(buildOwnedPostFilter(session), { projection: { markdown: 0 } })
.sort({ createdAt: -1 })
.limit(20)
.toArray();
return posts.map((post: any) => ({
...serializePost(post),
createdAtText: new Date(post.createdAt).toLocaleString("zh-CN", {
hour12: false,
timeZone: "Asia/Shanghai"
})
}));
}
async function fetchAvailableTags(session: Awaited<ReturnType<typeof verifySession>>) {
const db = await getDb();
const tags = await db
.collection("posts")
.aggregate([
{ $match: buildOwnedPostFilter(session) },
{ $unwind: "$tags" },
{ $group: { _id: "$tags", count: { $sum: 1 } } },
{ $sort: { count: -1, _id: 1 } }
])
.toArray();
return tags.map((item: any) => item._id).filter(Boolean);
}
async function fetchPublishQuota(session: Awaited<ReturnType<typeof verifySession>>) {
const user = await findUserById(session?.uid);
const limit = getEffectiveDailyPostLimit(user || undefined);
const { startIso, endIso } = getShanghaiDayRange();
const todayCount = session?.uid
? await getDb().then((db) =>
db.collection("posts").countDocuments({
ownerId: session.uid,
createdAt: { $gte: startIso, $lt: endIso }
})
)
: 0;
return { limit, todayCount };
}
async function fetchManagedUsers(): Promise<ManagedUser[]> {
const db = await getDb();
const { startIso, endIso } = getShanghaiDayRange();
const users = await db
.collection("users")
.find(
{},
{
projection: {
username: 1,
displayName: 1,
role: 1,
dailyPostLimit: 1
}
}
)
.sort({ createdAt: 1 })
.toArray();
const posts = await db
.collection("posts")
.find({}, { projection: { slug: 1, title: 1, createdAt: 1, ownerId: 1, author: 1 } })
.sort({ createdAt: -1 })
.toArray();
const authorToUserId = new Map<string, string>();
users.forEach((user: any) => {
const id = user._id?.toString();
if (!id) return;
if (user.username) authorToUserId.set(String(user.username).trim().toLowerCase(), id);
if (user.displayName) authorToUserId.set(String(user.displayName).trim().toLowerCase(), id);
});
const postCountMap = new Map<string, number>();
const todayCountMap = new Map<string, number>();
const postsByOwner = new Map<string, Array<{ slug: string; title: string; createdAt: string }>>();
posts.forEach((post: any) => {
const resolvedOwnerId =
post.ownerId || authorToUserId.get(String(post.author || "").trim().toLowerCase());
if (!resolvedOwnerId) return;
const list = postsByOwner.get(resolvedOwnerId) ?? [];
list.push({
slug: post.slug,
title: post.title ?? "未命名",
createdAt: post.createdAt ?? new Date().toISOString()
});
postsByOwner.set(resolvedOwnerId, list);
postCountMap.set(resolvedOwnerId, (postCountMap.get(resolvedOwnerId) ?? 0) + 1);
if (post.createdAt >= startIso && post.createdAt < endIso) {
todayCountMap.set(resolvedOwnerId, (todayCountMap.get(resolvedOwnerId) ?? 0) + 1);
}
});
return users
.map((user: any) => {
const id = user._id?.toString();
if (!id) return null;
return {
id,
username: user.username ?? "",
displayName: user.displayName ?? user.username ?? "",
role: user.role === "admin" ? "admin" : "user",
dailyPostLimit: getEffectiveDailyPostLimit(user),
postCount: postCountMap.get(id) ?? 0,
todayPostCount: todayCountMap.get(id) ?? 0,
posts: postsByOwner.get(id) ?? []
};
})
.filter(Boolean) as ManagedUser[];
}
export default async function AdminPage() {
const token = cookies().get(cookieName)?.value;
const session = await verifySession(token);
const adminView = isAdminSession(session);
const [recentPosts, availableTags, publishQuota, managedUsers] = await Promise.all([
fetchRecentPosts(session),
fetchAvailableTags(session),
fetchPublishQuota(session),
adminView ? fetchManagedUsers() : Promise.resolve([] as ManagedUser[])
]);
return (
<div className="space-y-6">
<section className="rounded-2xl bg-white/80 p-5 shadow-sm ring-1 ring-slate-100">
<div className="space-y-2">
<h1 className="text-2xl font-semibold text-slate-900"></h1>
<p className="text-sm text-slate-500">
</p>
</div>
</section>
<CreatePostForm
availableTags={availableTags}
publishLimit={publishQuota.limit}
todayCount={publishQuota.todayCount}
/>
<AdminPostList initialPosts={recentPosts} canDelete={false} />
{adminView ? <AdminUserManager initialUsers={managedUsers} currentUserId={session?.uid || ""} /> : null}
</div>
);
}