127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
import { ZodError } from "zod";
|
|
import type { NextApiRequest, NextApiResponse } from "next";
|
|
import { currentAccount } from "@/lib/account";
|
|
import { trustedOrigin } from "@/lib/account-crypto";
|
|
import { documentInput, parseDocumentXml } from "@/lib/guidance-input";
|
|
import {
|
|
discoverDocuments,
|
|
publishDocument,
|
|
resolveDocument,
|
|
withdrawDocument,
|
|
} from "@/lib/guidance-store";
|
|
import { guidanceXml } from "@/content/guides/catalog";
|
|
export default async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse,
|
|
) {
|
|
res.setHeader("Cache-Control", "no-store");
|
|
const path = req.query.path || [];
|
|
if (!Array.isArray(path) || path.length > 1) return res.status(404).end();
|
|
const id = path[0];
|
|
try {
|
|
if (req.method === "GET") {
|
|
if (!id) {
|
|
const author = req.query.author;
|
|
if (
|
|
author !== undefined &&
|
|
(typeof author !== "string" ||
|
|
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
author,
|
|
))
|
|
)
|
|
return res.status(400).json({ error: "作者 ID 无效" });
|
|
return res.json({ documents: await discoverDocuments(author) });
|
|
}
|
|
const revision = req.query.revision;
|
|
if (
|
|
revision !== undefined &&
|
|
(typeof revision !== "string" ||
|
|
!/^[1-9][0-9]{0,9}$/.test(revision) ||
|
|
Number(revision) > 2147483647)
|
|
)
|
|
return res.status(400).json({ error: "版本无效" });
|
|
const doc = await resolveDocument(
|
|
id,
|
|
revision === undefined ? undefined : Number(revision),
|
|
);
|
|
if (!doc) return res.status(404).json({ error: "文档或精确版本不存在" });
|
|
if (req.query.format === "xml") {
|
|
res.setHeader("Content-Type", "application/xml; charset=utf-8");
|
|
return res.send(guidanceXml(doc));
|
|
}
|
|
return res.json({ document: doc });
|
|
}
|
|
if (!["POST", "DELETE"].includes(req.method || "")) {
|
|
res.setHeader("Allow", "GET, POST, DELETE");
|
|
return res.status(405).end();
|
|
}
|
|
const request = new Request(
|
|
new URL(
|
|
req.url || "/",
|
|
process.env.ACCOUNT_PUBLIC_ORIGIN || "http://" + req.headers.host,
|
|
),
|
|
{
|
|
headers: new Headers(
|
|
Object.entries(req.headers).filter(
|
|
([, v]) => typeof v === "string",
|
|
) as [string, string][],
|
|
),
|
|
},
|
|
);
|
|
if (!trustedOrigin(request))
|
|
return res.status(403).json({ error: "请求来源不受信任" });
|
|
const user = await currentAccount(request);
|
|
if (!user) return res.status(401).json({ error: "请先登录已验证邮箱账户" });
|
|
if (req.method === "DELETE")
|
|
return id && (await withdrawDocument(user.id, id))
|
|
? res.json({ ok: true })
|
|
: res.status(404).json({ error: "无权删除或不存在" });
|
|
const input =
|
|
typeof req.body === "string"
|
|
? parseDocumentXml(req.body)
|
|
: documentInput.parse(req.body);
|
|
if (id === "preview") {
|
|
const links = await Promise.all(
|
|
input.links.map(async (l) => ({
|
|
...l,
|
|
resolved: !!(await resolveDocument(l.target, l.revision)),
|
|
})),
|
|
);
|
|
return res.json({ document: input, xml: guidanceXml(input), links });
|
|
}
|
|
if (id && id !== input.id)
|
|
return res.status(400).json({ error: "文档 ID 不匹配" });
|
|
return res
|
|
.status(201)
|
|
.json({ document: await publishDocument(user.id, input) });
|
|
} catch (e) {
|
|
if (e instanceof ZodError)
|
|
return res
|
|
.status(400)
|
|
.json({
|
|
error: "输入不符合协议,请检查以下字段",
|
|
fields: e.issues.slice(0, 12).map((i) => i.path.join(".") || "文档"),
|
|
});
|
|
const message = e instanceof Error ? e.message : "";
|
|
if (message === "INVALID_ID")
|
|
return res
|
|
.status(400)
|
|
.json({
|
|
error: "文档 ID 必须以 user-你的账户ID- 开头,请使用编辑器生成的前缀",
|
|
});
|
|
if (message === "FORBIDDEN")
|
|
return res.status(403).json({ error: "仅作者可编辑" });
|
|
if (message === "CONFLICT" || (e as { code?: string }).code === "23505")
|
|
return res
|
|
.status(409)
|
|
.json({ error: "版本冲突:请加载最新版本,已发布版本不可修改" });
|
|
if (
|
|
e instanceof Error &&
|
|
(e.name === "ZodError" || /XML|实体|元素|属性|协议|链接/.test(message))
|
|
)
|
|
return res.status(400).json({ error: "输入不符合协议" });
|
|
return res.status(503).json({ error: "文档服务暂不可用" });
|
|
}
|
|
}
|
|
export const config = { api: { bodyParser: { sizeLimit: "128kb" } } };
|