0.3.5 共享功能修复
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import crypto from "crypto";
|
||||
import { api } from "@/lib/convex/api";
|
||||
import { HttpError } from "@/lib/auth/authContext";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const ONLYOFFICE_INTERNAL_URL = (process.env.ONLYOFFICE_INTERNAL_URL || "http://127.0.0.1:8081").replace(/\/+$/, "");
|
||||
|
||||
const base64Url = (input: Buffer | string) =>
|
||||
Buffer.from(input)
|
||||
.toString("base64")
|
||||
.replace(/=/g, "")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
|
||||
const signHs256 = (payload: unknown, secret: string) => {
|
||||
const header = { alg: "HS256", typ: "JWT" };
|
||||
const headerPart = base64Url(JSON.stringify(header));
|
||||
const payloadPart = base64Url(JSON.stringify(payload));
|
||||
const signingInput = `${headerPart}.${payloadPart}`;
|
||||
const signature = crypto.createHmac("sha256", secret).update(signingInput).digest();
|
||||
return `${signingInput}.${base64Url(signature)}`;
|
||||
};
|
||||
|
||||
const normalizeSecret = (raw: string) => {
|
||||
const trimmed = String(raw || "").trim();
|
||||
if (!trimmed) return "";
|
||||
// 兼容用户把 .env 的值写成 "xxx" / 'xxx'
|
||||
if (
|
||||
(trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length >= 2) ||
|
||||
(trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2)
|
||||
) {
|
||||
return trimmed.slice(1, -1).trim();
|
||||
}
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let auth;
|
||||
let client;
|
||||
try {
|
||||
const authed = await getAuthedConvexClient();
|
||||
auth = authed.auth;
|
||||
client = authed.client;
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
}
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const assetId = String(searchParams.get("assetId") || "").trim();
|
||||
const key = String(searchParams.get("key") || "").trim();
|
||||
if (!assetId) return NextResponse.json({ error: "缺少 assetId" }, { status: 400 });
|
||||
if (!key) return NextResponse.json({ error: "缺少 key" }, { status: 400 });
|
||||
|
||||
const asset = await client.query(api.mediaAssets.getById, { userId: auth.userId, id: assetId });
|
||||
if (!asset) {
|
||||
return NextResponse.json({ error: "资源不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
// 只读权限:不允许触发 forcesave(避免只读用户间接写回存储)
|
||||
const docId = String((asset as any).document_id || "").trim();
|
||||
if (!docId) {
|
||||
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
|
||||
}
|
||||
const doc = await client.query(api.documents.getMeta, { id: docId });
|
||||
if (!doc || (doc as any).can_edit === false) {
|
||||
return NextResponse.json({ error: "无权修改(只读共享的文件)" }, { status: 403 });
|
||||
}
|
||||
|
||||
const payload = { c: "forcesave", key, userdata: `asset:${assetId}` };
|
||||
const secret = normalizeSecret(process.env.ONLYOFFICE_JWT_SECRET || "");
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tryJson = async (res: Response) => (await res.json().catch(() => null)) as any;
|
||||
|
||||
try {
|
||||
// 优先按文档推荐:使用 /command + token
|
||||
if (secret) {
|
||||
const token = signHs256(payload, secret);
|
||||
const r = await fetch(`${ONLYOFFICE_INTERNAL_URL}/command`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
const j = await tryJson(r);
|
||||
if (r.ok && Number(j?.error ?? 0) === 0) {
|
||||
return NextResponse.json({ ok: true, via: "command", result: j });
|
||||
}
|
||||
|
||||
// 兜底:部分环境可能暴露 /forcesave 直连接口
|
||||
const r2 = await fetch(`${ONLYOFFICE_INTERNAL_URL}/forcesave`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const j2 = await tryJson(r2);
|
||||
if (r2.ok && Number(j2?.error ?? 0) === 0) {
|
||||
return NextResponse.json({ ok: true, via: "forcesave", result: j2 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "触发 forcesave 失败", detail: { command: j, forcesave: j2 } },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
|
||||
// JWT 未启用:尝试 /forcesave 直连
|
||||
const r = await fetch(`${ONLYOFFICE_INTERNAL_URL}/forcesave`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const j = await tryJson(r);
|
||||
if (r.ok && Number(j?.error ?? 0) === 0) {
|
||||
return NextResponse.json({ ok: true, via: "forcesave", result: j });
|
||||
}
|
||||
|
||||
// 最后兜底:部分部署可能仍接受不带 token 的 /command(不保证)
|
||||
const r2 = await fetch(`${ONLYOFFICE_INTERNAL_URL}/command`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const j2 = await tryJson(r2);
|
||||
if (r2.ok && Number(j2?.error ?? 0) === 0) {
|
||||
return NextResponse.json({ ok: true, via: "command_no_token", result: j2 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "触发 forcesave 失败", detail: { forcesave: j, command: j2 } },
|
||||
{ status: 502 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[onlyoffice/forcesave] failed:", error);
|
||||
return NextResponse.json({ error: "触发 forcesave 失败" }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user