feat: rotate signed Memos webhooks

This commit is contained in:
2026-07-19 05:55:43 +08:00
parent 2af99a4b43
commit e9190da834
4 changed files with 20 additions and 6 deletions
+11 -2
View File
@@ -1,15 +1,24 @@
import { NextResponse } from "next/server";
import { requireUser } from "@/lib/auth";
import { db } from "@/lib/db";
import { decrypt, encrypt } from "@/lib/crypto";
import { createUserWebhook, deleteUserWebhook, getMemosIdentity } from "@/lib/memos";
import { createWebhookSecret, webhookSecretHash } from "@/lib/webhook";
import { requireSameOrigin } from "@/lib/security";
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
try {
requireSameOrigin(request); const user = await requireUser(); const { id: rawId } = await params; const id = Number(rawId);
const source = db.prepare("SELECT id,webhook_mode FROM sources WHERE id=? AND user_id=?").get(id, user.id) as { id: number; webhook_mode: string } | undefined;
const source = db.prepare("SELECT id,base_url,token_encrypted,remote_user,webhook_mode,webhook_remote_name FROM sources WHERE id=? AND user_id=?").get(id, user.id) as { id: number; base_url: string; token_encrypted: string; remote_user: string | null; webhook_mode: string; webhook_remote_name: string | null } | undefined;
if (!source) return NextResponse.json({ error: "Not found" }, { status: 404 });
if (source.webhook_mode === "signed") return NextResponse.json({ error: "這個 Webhook 已由 Memos 自動管理;請重新連接來源以重新建立。" }, { status: 409 });
if (source.webhook_mode === "signed") {
const token = decrypt(source.token_encrypted); const identity = source.remote_user ? { name: source.remote_user } : await getMemosIdentity(source.base_url, token); const pathSecret = createWebhookSecret(); const signingSecret = createWebhookSecret(); const publicOrigin = (process.env.NEXT_PUBLIC_APP_URL || new URL(request.url).origin).replace(/\/$/, "");
const remote = await createUserWebhook(source.base_url, token, identity.name, { url: `${publicOrigin}/api/sync/webhook/${id}/${pathSecret}`, displayName: "Mebbling", signingSecret });
try { if (source.webhook_remote_name) await deleteUserWebhook(source.base_url, token, identity.name, source.webhook_remote_name); }
catch (error) { try { await deleteUserWebhook(source.base_url, token, identity.name, remote.name); } catch {} throw error; }
db.prepare("UPDATE sources SET webhook_secret_hash=?,webhook_remote_name=?,webhook_signing_secret_encrypted=? WHERE id=?").run(webhookSecretHash(pathSecret), remote.name, encrypt(signingSecret), id);
return NextResponse.json({ rotated: true });
}
const secret = createWebhookSecret();
db.prepare("UPDATE sources SET webhook_secret_hash=? WHERE id=?").run(webhookSecretHash(secret), id);
const publicOrigin = (process.env.NEXT_PUBLIC_APP_URL || new URL(request.url).origin).replace(/\/$/, "");