Implement per-user post permissions and move stats into dedicated pages

This commit is contained in:
爱喝水的木子
2026-03-20 11:51:58 +08:00
parent 17f5f6adcb
commit 466b7c3fb6
29 changed files with 1416 additions and 475 deletions

View File

@@ -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 });
}

View File

@@ -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 });
}