93 lines
2.7 KiB
TypeScript
93 lines
2.7 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
||
import { z } from "zod";
|
||
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),
|
||
markdown: z.string().min(5),
|
||
cover: z.string().url().optional(),
|
||
tags: z.array(z.string().trim()).optional(),
|
||
signal: z.enum(OPC_SIGNAL_VALUES).default(DEFAULT_OPC_SIGNAL)
|
||
});
|
||
|
||
export async function GET() {
|
||
const db = await getDb();
|
||
const posts = await db
|
||
.collection("posts")
|
||
.find({}, { projection: { markdown: 0 } })
|
||
.sort({ createdAt: -1 })
|
||
.limit(50)
|
||
.toArray();
|
||
|
||
return NextResponse.json(
|
||
posts.map((post) => ({
|
||
...post,
|
||
author: post.author ?? "匿名",
|
||
_id: post._id?.toString()
|
||
}))
|
||
);
|
||
}
|
||
|
||
export async function POST(req: NextRequest) {
|
||
const json = await req.json().catch(() => ({}));
|
||
const parsed = postSchema.safeParse(json);
|
||
|
||
if (!parsed.success) {
|
||
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
||
}
|
||
|
||
const token = req.cookies.get(cookieName)?.value;
|
||
const session = await verifySession(token);
|
||
if (!session?.uid) {
|
||
return NextResponse.json({ error: "请先登录后再发布" }, { status: 401 });
|
||
}
|
||
|
||
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();
|
||
|
||
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: "生成内容标识失败" }, { status: 500 });
|
||
}
|
||
|
||
await db.collection("posts").insertOne({
|
||
...data,
|
||
author,
|
||
ownerId: session.uid,
|
||
slug,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
views: 0
|
||
});
|
||
|
||
return NextResponse.json({ ok: true, slug, todayCount: todayCount + 1, limit });
|
||
}
|