0.3.5 共享功能修复
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import supabaseAdmin from "@/lib/supabase/admin";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const ONLYOFFICE_CALLBACK_SECRET = String(process.env.ONLYOFFICE_CALLBACK_SECRET || "").trim();
|
||||
|
||||
type OnlyOfficeCallbackBody = {
|
||||
status?: number;
|
||||
url?: string;
|
||||
key?: string;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const isUuid = (value: string) =>
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
||||
|
||||
const resolveAssetObjectLocation = async (params: {
|
||||
bucket?: string | null;
|
||||
storagePath?: string | null;
|
||||
workspaceId?: string | null;
|
||||
fileName?: string | null;
|
||||
}) => {
|
||||
const { bucket, storagePath, workspaceId, fileName } = params;
|
||||
const ws = (workspaceId ?? "").trim();
|
||||
const fn = (fileName ?? "").trim();
|
||||
|
||||
if (!ws || !isUuid(ws) || !fn) return null;
|
||||
|
||||
try {
|
||||
const storage = supabaseAdmin.schema("storage");
|
||||
|
||||
const b = (bucket ?? "").trim();
|
||||
const p = (storagePath ?? "").trim();
|
||||
if (b && p) {
|
||||
const { data: exact, error: exactError } = await storage
|
||||
.from("objects")
|
||||
.select("bucket_id,name")
|
||||
.eq("bucket_id", b)
|
||||
.eq("name", p)
|
||||
.limit(1);
|
||||
if (!exactError && exact && exact.length > 0) {
|
||||
return { bucket: b, path: p };
|
||||
}
|
||||
}
|
||||
|
||||
const pattern = `${ws}/%/${fn}`;
|
||||
const { data: candidates, error: candError } = await storage
|
||||
.from("objects")
|
||||
.select("bucket_id,name,created_at")
|
||||
.like("name", pattern)
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(20);
|
||||
|
||||
if (candError || !candidates || candidates.length === 0) return null;
|
||||
|
||||
const exactCandidate =
|
||||
candidates.find((o) => String(o?.name || "").endsWith(`/${fn}`)) ?? candidates[0];
|
||||
if (!exactCandidate?.bucket_id || !exactCandidate?.name) return null;
|
||||
return { bucket: String(exactCandidate.bucket_id), path: String(exactCandidate.name) };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const assetId = searchParams.get("assetId") || "";
|
||||
|
||||
// 说明:ONLYOFFICE 回调由文档服务器触发,不一定携带用户态;这里提供一个可选的共享密钥校验。
|
||||
// 若未配置 ONLYOFFICE_CALLBACK_SECRET,则保持兼容不校验。
|
||||
if (ONLYOFFICE_CALLBACK_SECRET) {
|
||||
const got = normalizeSecret(searchParams.get("token") || "");
|
||||
const expected = normalizeSecret(ONLYOFFICE_CALLBACK_SECRET);
|
||||
if (!got || got !== expected) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
const body = (await request.json().catch(() => null)) as OnlyOfficeCallbackBody | null;
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: 0 });
|
||||
}
|
||||
|
||||
const status = Number(body.status ?? -1);
|
||||
// 说明:仅在文档需要保存时处理(2=ready for saving;6=force save)。
|
||||
if (status !== 2 && status !== 6) {
|
||||
return NextResponse.json({ error: 0 });
|
||||
}
|
||||
|
||||
if (!assetId) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
if (!body.url) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: asset, error: assetError } = await supabaseAdmin
|
||||
.from("media_assets")
|
||||
.select("*")
|
||||
.eq("id", assetId)
|
||||
.single();
|
||||
if (assetError || !asset) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
const loc = await resolveAssetObjectLocation({
|
||||
bucket: (asset.bucket as string | null) ?? null,
|
||||
storagePath: (asset.storage_path as string | null) ?? null,
|
||||
workspaceId: (asset.workspace_id as string | null) ?? null,
|
||||
fileName: (asset.file_name as string | null) ?? null,
|
||||
});
|
||||
if (!loc) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
const upstream = await fetch(body.url, { method: "GET", redirect: "follow" });
|
||||
if (!upstream.ok) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
const buf = Buffer.from(await upstream.arrayBuffer());
|
||||
|
||||
const contentType = String(asset.mime_type || "application/octet-stream");
|
||||
const { error: uploadError } = await supabaseAdmin.storage.from(loc.bucket).upload(loc.path, buf, {
|
||||
contentType,
|
||||
upsert: true,
|
||||
});
|
||||
if (uploadError) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
// 说明:桌面端(Supabase)模式下,文件 URL 通常是短期 signedUrl;
|
||||
// 覆盖同一 storage_path 的对象即可让后续刷新/下载拿到最新文件。
|
||||
return NextResponse.json({ error: 0 });
|
||||
} catch (error) {
|
||||
console.error("[onlyoffice/callback] supabase writeback failed:", error);
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function OnlyOfficePage() {
|
||||
} catch {
|
||||
return base;
|
||||
}
|
||||
}, [fileUrl, storageHostOverride]);
|
||||
}, [fileUrl, proxyOrigin, runtimeConfig.supabaseUrl, storageHostOverride]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!baseUrl) {
|
||||
@@ -144,6 +144,16 @@ export default function OnlyOfficePage() {
|
||||
editorConfig: {
|
||||
mode: mode === "view" ? "view" : "edit",
|
||||
lang: "zh-CN",
|
||||
// 说明:ONLYOFFICE 文档服务器会通过 callbackUrl 回传保存事件;
|
||||
// 桌面端(standalone)这里必须配置,否则“看似已保存”但不会写回原文件。
|
||||
callbackUrl: assetId
|
||||
? (() => {
|
||||
const base = proxyOrigin || window.location.origin;
|
||||
const cb = new URL("/api/onlyoffice/callback", base);
|
||||
cb.searchParams.set("assetId", assetId);
|
||||
return cb.toString();
|
||||
})()
|
||||
: undefined,
|
||||
customization: {
|
||||
feedback: { visible: false },
|
||||
},
|
||||
@@ -192,7 +202,7 @@ export default function OnlyOfficePage() {
|
||||
.catch((err: Error) => {
|
||||
setError(err.message);
|
||||
});
|
||||
}, [baseUrl, fileName, fileType, mode, resolvedFileUrl, targetDocType]);
|
||||
}, [assetId, baseUrl, fileName, fileType, mode, proxyOrigin, resolvedFileUrl, targetDocType]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user