This commit is contained in:
爱喝水的木子
2026-03-13 16:28:51 +08:00
commit bfdf4843e1
38 changed files with 9490 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
import { NextResponse } from "next/server";
import { getDb } from "@/lib/mongo";
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();
return NextResponse.json({
total,
latest: latest[0] || null,
top
});
}

27
app/api/login/route.ts Normal file
View File

@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
import { signSession, cookieName } from "@/lib/auth";
export async function POST(req: NextRequest) {
const body = await req.json().catch(() => ({}));
const password = body.password as string | undefined;
const target = process.env.ADMIN_PASS;
if (!target) {
return NextResponse.json({ error: "ADMIN_PASS is not set on server" }, { status: 500 });
}
if (!password || password !== target) {
return NextResponse.json({ error: "密码错误" }, { status: 401 });
}
const token = await signSession({ role: "admin", iat: Date.now() });
const res = NextResponse.json({ ok: true });
res.cookies.set(cookieName, token, {
httpOnly: true,
sameSite: "lax",
secure: true,
maxAge: 60 * 60 * 24 * 30,
path: "/"
});
return res;
}

View File

@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from "next/server";
import { getDb } from "@/lib/mongo";
import { ObjectId } from "mongodb";
import { z } from "zod";
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" }
);
if (!post.value) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json({
...post.value,
_id: (post.value._id as ObjectId)?.toString()
});
}
export async function PATCH(req: NextRequest, { params }: { params: { slug: string } }) {
const body = await req.json().catch(() => ({}));
const schema = z.object({
title: z.string().min(2).max(80).optional(),
markdown: z.string().min(5).optional(),
cover: z.string().url().nullable().optional(),
tags: z.array(z.string().trim()).optional(),
author: z.string().optional()
});
const parsed = schema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
}
const data = parsed.data;
const set: Record<string, unknown> = {
updatedAt: new Date().toISOString()
};
const unset: Record<string, unknown> = {};
if (typeof data.title === "string") set.title = data.title;
if (typeof data.markdown === "string") set.markdown = data.markdown;
if (data.cover === null) {
unset.cover = "";
} else if (typeof data.cover === "string") {
set.cover = data.cover;
}
if (Array.isArray(data.tags)) set.tags = data.tags;
if (typeof data.author === "string") set.author = data.author;
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 });
}
return NextResponse.json({ ok: true });
}
export async function DELETE(_: NextRequest, { params }: { params: { slug: string } }) {
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 });
}
return NextResponse.json({ ok: true });
}

64
app/api/posts/route.ts Normal file
View File

@@ -0,0 +1,64 @@
import { NextRequest, NextResponse } from "next/server";
import { getDb } from "@/lib/mongo";
import { z } from "zod";
import { generateSlug } from "@/lib/slug";
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(),
author: z.string().default("admin")
});
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((p) => ({
...p,
_id: p._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 data = parsed.data;
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 });
}
const doc = {
...data,
slug,
createdAt: now,
updatedAt: now,
views: 0
};
await db.collection("posts").insertOne(doc);
return NextResponse.json({ ok: true, slug });
}