Implement per-user post permissions and move stats into dedicated pages
This commit is contained in:
@@ -1,16 +1,27 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getDb } from "@/lib/mongo";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookieName, isAdminSession, verifySession } from "@/lib/auth";
|
||||
import { buildOwnedPostFilter } from "@/lib/posts";
|
||||
import { fetchAuthorBreakdown, fetchDailyStats, fetchStatsSummary, fetchTagStats } from "@/lib/stats";
|
||||
|
||||
export async function GET() {
|
||||
const db = await getDb();
|
||||
const collection = db.collection("posts");
|
||||
const total = await collection.countDocuments();
|
||||
const latest = await collection.find({}, { projection: { title: 1, createdAt: 1 } }).sort({ createdAt: -1 }).limit(1).toArray();
|
||||
const top = await collection.find({}, { projection: { title: 1, views: 1, slug: 1 } }).sort({ views: -1 }).limit(3).toArray();
|
||||
export async function GET(req: NextRequest) {
|
||||
const token = req.cookies.get(cookieName)?.value;
|
||||
const session = await verifySession(token);
|
||||
const personalFilter = buildOwnedPostFilter(session);
|
||||
const adminView = isAdminSession(session);
|
||||
|
||||
const [mine, overall, myTags, myDaily, authors] = await Promise.all([
|
||||
fetchStatsSummary(personalFilter),
|
||||
fetchStatsSummary({}),
|
||||
fetchTagStats(personalFilter, 8),
|
||||
fetchDailyStats(personalFilter, 7),
|
||||
adminView ? fetchAuthorBreakdown() : Promise.resolve([])
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
total,
|
||||
latest: latest[0] || null,
|
||||
top
|
||||
mine,
|
||||
overall,
|
||||
myTags,
|
||||
myDaily,
|
||||
authors
|
||||
});
|
||||
}
|
||||
|
||||
86
app/api/admin/users/[userId]/route.ts
Normal file
86
app/api/admin/users/[userId]/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { z } from "zod";
|
||||
import { cookieName, isAdminSession, verifySession } from "@/lib/auth";
|
||||
import { getDb } from "@/lib/mongo";
|
||||
import { DEFAULT_DAILY_POST_LIMIT } from "@/lib/users";
|
||||
|
||||
async function requireAdmin(req: NextRequest) {
|
||||
const token = req.cookies.get(cookieName)?.value;
|
||||
const session = await verifySession(token);
|
||||
if (!isAdminSession(session)) {
|
||||
return null;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: { params: { userId: string } }) {
|
||||
const session = await requireAdmin(req);
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "未授权" }, { status: 401 });
|
||||
}
|
||||
if (!ObjectId.isValid(params.userId)) {
|
||||
return NextResponse.json({ error: "用户 ID 无效" }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const schema = z.object({
|
||||
dailyPostLimit: z.number().int().min(0).max(1000)
|
||||
});
|
||||
const parsed = schema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const result = await db.collection("users").updateOne(
|
||||
{ _id: new ObjectId(params.userId) },
|
||||
{ $set: { dailyPostLimit: parsed.data.dailyPostLimit } }
|
||||
);
|
||||
if (result.matchedCount === 0) {
|
||||
return NextResponse.json({ error: "用户不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
dailyPostLimit: parsed.data.dailyPostLimit ?? DEFAULT_DAILY_POST_LIMIT
|
||||
});
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest, { params }: { params: { userId: string } }) {
|
||||
const session = await requireAdmin(req);
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "未授权" }, { status: 401 });
|
||||
}
|
||||
if (!ObjectId.isValid(params.userId)) {
|
||||
return NextResponse.json({ error: "用户 ID 无效" }, { status: 400 });
|
||||
}
|
||||
if (session.uid === params.userId) {
|
||||
return NextResponse.json({ error: "不能删除当前登录的管理员账号" }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const user = await db.collection("users").findOne({ _id: new ObjectId(params.userId) });
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "用户不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
await db.collection("posts").deleteMany({
|
||||
$or: [
|
||||
{ ownerId: params.userId },
|
||||
{
|
||||
$and: [
|
||||
{ $or: [{ ownerId: { $exists: false } }, { ownerId: null }] },
|
||||
{
|
||||
author: {
|
||||
$in: [user.displayName, user.username].filter(Boolean)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
await db.collection("users").deleteOne({ _id: new ObjectId(params.userId) });
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { signSession, cookieName } from "@/lib/auth";
|
||||
import { z } from "zod";
|
||||
import { signSession, cookieName, isAdminName } from "@/lib/auth";
|
||||
import { getDb } from "@/lib/mongo";
|
||||
import { verifyPassword } from "@/lib/password";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json().catch(() => ({}));
|
||||
@@ -13,7 +13,7 @@ export async function POST(req: NextRequest) {
|
||||
const parsed = schema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "请输入正确的用户名与密码" }, { status: 400 });
|
||||
return NextResponse.json({ error: "用户名或密码格式不正确" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { username, password } = parsed.data;
|
||||
@@ -29,13 +29,20 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const name = user.displayName || user.username || username;
|
||||
const role =
|
||||
user.role === "admin" || user.role === "user"
|
||||
? user.role
|
||||
: isAdminName(user.username) || isAdminName(name)
|
||||
? "admin"
|
||||
: "user";
|
||||
const exp = Date.now() + 24 * 60 * 60 * 1000;
|
||||
const token = await signSession({
|
||||
role: "admin",
|
||||
role,
|
||||
iat: Date.now(),
|
||||
exp,
|
||||
uid: user._id?.toString(),
|
||||
name
|
||||
name,
|
||||
username: user.username || username
|
||||
});
|
||||
const res = NextResponse.json({ ok: true, name });
|
||||
res.cookies.set(cookieName, token, {
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDb } from "@/lib/mongo";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { z } from "zod";
|
||||
import { cookieName, verifySession } from "@/lib/auth";
|
||||
import { getDb } from "@/lib/mongo";
|
||||
import { OPC_SIGNAL_VALUES } from "@/lib/opc";
|
||||
import { canDeletePost, canEditPost, serializePost } from "@/lib/posts";
|
||||
|
||||
async function getSessionFromRequest(req: NextRequest) {
|
||||
const token = req.cookies.get(cookieName)?.value;
|
||||
return verifySession(token);
|
||||
}
|
||||
|
||||
export async function GET(_: NextRequest, { params }: { params: { slug: string } }) {
|
||||
const db = await getDb();
|
||||
const post = await db.collection("posts").findOneAndUpdate(
|
||||
{ slug: params.slug },
|
||||
{ $inc: { views: 1 } },
|
||||
{ returnDocument: "after" }
|
||||
);
|
||||
const post = await db.collection("posts").findOne({ slug: params.slug });
|
||||
|
||||
if (!post || !post.value) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
if (!post) {
|
||||
return NextResponse.json({ error: "内容不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...post.value,
|
||||
author: post.value.author ?? "佚名",
|
||||
_id: (post.value._id as ObjectId)?.toString()
|
||||
});
|
||||
await db.collection("posts").updateOne({ _id: post._id }, { $inc: { views: 1 } });
|
||||
return NextResponse.json(serializePost({ ...post, views: (post.views ?? 0) + 1 }));
|
||||
}
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: { params: { slug: string } }) {
|
||||
const session = await getSessionFromRequest(req);
|
||||
const db = await getDb();
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const schema = z.object({
|
||||
title: z.string().min(2).max(80).optional(),
|
||||
@@ -37,6 +38,14 @@ export async function PATCH(req: NextRequest, { params }: { params: { slug: stri
|
||||
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
||||
}
|
||||
|
||||
const existingPost = await db.collection("posts").findOne({ slug: params.slug });
|
||||
if (!existingPost) {
|
||||
return NextResponse.json({ error: "内容不存在" }, { status: 404 });
|
||||
}
|
||||
if (!canEditPost(existingPost, session)) {
|
||||
return NextResponse.json({ error: "只能修改自己发布的内容" }, { status: 403 });
|
||||
}
|
||||
|
||||
const data = parsed.data;
|
||||
const set: Record<string, unknown> = {
|
||||
updatedAt: new Date().toISOString()
|
||||
@@ -54,21 +63,26 @@ export async function PATCH(req: NextRequest, { params }: { params: { slug: stri
|
||||
if (typeof data.signal === "string") set.signal = data.signal;
|
||||
|
||||
const update: Record<string, unknown> = { $set: set };
|
||||
if (Object.keys(unset).length > 0) update.$unset = unset;
|
||||
|
||||
const db = await getDb();
|
||||
const result = await db.collection("posts").updateOne({ slug: params.slug }, update);
|
||||
if (result.matchedCount === 0) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
if (Object.keys(unset).length > 0) {
|
||||
update.$unset = unset;
|
||||
}
|
||||
|
||||
await db.collection("posts").updateOne({ _id: existingPost._id }, update);
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
export async function DELETE(_: NextRequest, { params }: { params: { slug: string } }) {
|
||||
export async function DELETE(req: NextRequest, { params }: { params: { slug: string } }) {
|
||||
const session = await getSessionFromRequest(req);
|
||||
const db = await getDb();
|
||||
const result = await db.collection("posts").deleteOne({ slug: params.slug });
|
||||
if (result.deletedCount === 0) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
const existingPost = await db.collection("posts").findOne({ slug: params.slug });
|
||||
|
||||
if (!existingPost) {
|
||||
return NextResponse.json({ error: "内容不存在" }, { status: 404 });
|
||||
}
|
||||
if (!canDeletePost(existingPost, session)) {
|
||||
return NextResponse.json({ error: "只有管理员可以删除内容" }, { status: 403 });
|
||||
}
|
||||
|
||||
await db.collection("posts").deleteOne({ _id: existingPost._id });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDb } from "@/lib/mongo";
|
||||
import { z } from "zod";
|
||||
import { generateSlug } from "@/lib/slug";
|
||||
import { DEFAULT_OPC_SIGNAL, OPC_SIGNAL_VALUES } from "@/lib/opc";
|
||||
import { cookieName, verifySession } from "@/lib/auth";
|
||||
import { getDb } from "@/lib/mongo";
|
||||
import { DEFAULT_OPC_SIGNAL, OPC_SIGNAL_VALUES } from "@/lib/opc";
|
||||
import { generateSlug } from "@/lib/slug";
|
||||
import { findUserById, getEffectiveDailyPostLimit, getShanghaiDayRange } from "@/lib/users";
|
||||
|
||||
const postSchema = z.object({
|
||||
title: z.string().min(2).max(80),
|
||||
@@ -23,10 +24,10 @@ export async function GET() {
|
||||
.toArray();
|
||||
|
||||
return NextResponse.json(
|
||||
posts.map((p) => ({
|
||||
...p,
|
||||
author: p.author ?? "佚名",
|
||||
_id: p._id?.toString()
|
||||
posts.map((post) => ({
|
||||
...post,
|
||||
author: post.author ?? "匿名",
|
||||
_id: post._id?.toString()
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -39,36 +40,53 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
||||
}
|
||||
|
||||
const data = parsed.data;
|
||||
const token = req.cookies.get(cookieName)?.value;
|
||||
const session = await verifySession(token);
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
if (!session?.uid) {
|
||||
return NextResponse.json({ error: "请先登录后再发布" }, { status: 401 });
|
||||
}
|
||||
const author = session.name ?? "佚名";
|
||||
|
||||
const user = await findUserById(session.uid);
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "用户不存在或已被删除" }, { status: 401 });
|
||||
}
|
||||
|
||||
const limit = getEffectiveDailyPostLimit(user);
|
||||
const { startIso, endIso } = getShanghaiDayRange();
|
||||
const db = await getDb();
|
||||
const todayCount = await db.collection("posts").countDocuments({
|
||||
ownerId: session.uid,
|
||||
createdAt: { $gte: startIso, $lt: endIso }
|
||||
});
|
||||
if (todayCount >= limit) {
|
||||
return NextResponse.json({ error: `今日发布数量已达到上限(${limit})` }, { status: 429 });
|
||||
}
|
||||
|
||||
const data = parsed.data;
|
||||
const author = session.name ?? "匿名";
|
||||
const now = new Date().toISOString();
|
||||
let slug = generateSlug();
|
||||
const db = await getDb();
|
||||
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
const exists = await db.collection("posts").findOne({ slug });
|
||||
if (!exists) break;
|
||||
slug = generateSlug();
|
||||
}
|
||||
|
||||
const existsAfter = await db.collection("posts").findOne({ slug });
|
||||
if (existsAfter) {
|
||||
return NextResponse.json({ error: "Failed to allocate slug" }, { status: 500 });
|
||||
return NextResponse.json({ error: "生成内容标识失败" }, { status: 500 });
|
||||
}
|
||||
|
||||
const doc = {
|
||||
await db.collection("posts").insertOne({
|
||||
...data,
|
||||
author,
|
||||
ownerId: session.uid,
|
||||
slug,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
views: 0
|
||||
};
|
||||
});
|
||||
|
||||
await db.collection("posts").insertOne(doc);
|
||||
return NextResponse.json({ ok: true, slug });
|
||||
return NextResponse.json({ ok: true, slug, todayCount: todayCount + 1, limit });
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { signSession, cookieName, isAdminName } from "@/lib/auth";
|
||||
import { DEFAULT_DAILY_POST_LIMIT } from "@/lib/users";
|
||||
import { getDb } from "@/lib/mongo";
|
||||
import { hashPassword } from "@/lib/password";
|
||||
import { signSession, cookieName } from "@/lib/auth";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json().catch(() => ({}));
|
||||
@@ -13,11 +14,13 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
const parsed = schema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "请填写有效的用户名与密码" }, { status: 400 });
|
||||
return NextResponse.json({ error: "用户名或密码格式不正确" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { username, password, displayName } = parsed.data;
|
||||
const usernameLower = username.toLowerCase();
|
||||
const resolvedDisplayName = displayName || username;
|
||||
const role = isAdminName(username) || isAdminName(resolvedDisplayName) ? "admin" : "user";
|
||||
const db = await getDb();
|
||||
|
||||
const exists = await db.collection("users").findOne({ usernameLower });
|
||||
@@ -30,7 +33,9 @@ export async function POST(req: NextRequest) {
|
||||
const doc = {
|
||||
username,
|
||||
usernameLower,
|
||||
displayName: displayName || username,
|
||||
displayName: resolvedDisplayName,
|
||||
role,
|
||||
dailyPostLimit: DEFAULT_DAILY_POST_LIMIT,
|
||||
passwordHash: hash,
|
||||
passwordSalt: salt,
|
||||
createdAt: now
|
||||
@@ -41,11 +46,12 @@ export async function POST(req: NextRequest) {
|
||||
const name = doc.displayName;
|
||||
const exp = Date.now() + 24 * 60 * 60 * 1000;
|
||||
const token = await signSession({
|
||||
role: "admin",
|
||||
role,
|
||||
iat: Date.now(),
|
||||
exp,
|
||||
uid: result.insertedId?.toString(),
|
||||
name
|
||||
name,
|
||||
username
|
||||
});
|
||||
const res = NextResponse.json({ ok: true, name });
|
||||
res.cookies.set(cookieName, token, {
|
||||
|
||||
Reference in New Issue
Block a user