0.4.0 convex及界面修改
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { ConvexHttpClient } from "convex/browser";
|
||||
import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server";
|
||||
import { isDevAuthEnabled } from "@/lib/auth/devUser";
|
||||
|
||||
let cached: ConvexHttpClient | null = null;
|
||||
|
||||
@@ -31,6 +32,16 @@ export async function getConvexAuthedHttpClient(): Promise<ConvexHttpClient> {
|
||||
|
||||
const token = await convexAuthNextjsToken();
|
||||
if (!token) {
|
||||
// 说明:开发用户模式(MNOTE_DEV_AUTH=1)用于迁移/联调阶段的“免登录”体验。
|
||||
// 此时浏览器侧可能没有 Convex Auth cookies,但服务端仍需要能访问 Convex。
|
||||
// 若配置了自托管 Admin Key,则允许在开发用户模式下回退到 Admin Auth(仅本地/联调使用)。
|
||||
const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY;
|
||||
if (adminKey && isDevAuthEnabled()) {
|
||||
const client = new ConvexHttpClient(url);
|
||||
(client as any).setAdminAuth(adminKey);
|
||||
return client;
|
||||
}
|
||||
|
||||
throw new Error("未登录");
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
export const ASSETS_CHANGED_EVENT = "wolai:assets-changed";
|
||||
export const DOCUMENTS_CHANGED_EVENT = "wolai:documents-changed";
|
||||
export const ASSETS_RESTORED_EVENT = "wolai:assets-restored";
|
||||
|
||||
type AssetsChangedPayload = {
|
||||
docId?: string;
|
||||
@@ -12,6 +13,11 @@ type AssetsChangedPayload = {
|
||||
mindmapAssetIds?: string[];
|
||||
};
|
||||
|
||||
type AssetsRestoredPayload =
|
||||
| { docId: string; kind: "media"; assetId: string; asset?: unknown }
|
||||
| { docId: string; kind: "mindmap"; mindmapId: string }
|
||||
| { docId: string; kind: "table"; tableId: string };
|
||||
|
||||
export function emitAssetsChanged(
|
||||
docId?: string,
|
||||
asset?: unknown,
|
||||
@@ -24,6 +30,11 @@ export function emitAssetsChanged(
|
||||
window.dispatchEvent(new CustomEvent(ASSETS_CHANGED_EVENT, { detail }));
|
||||
}
|
||||
|
||||
export function emitAssetsRestored(payload: AssetsRestoredPayload) {
|
||||
if (typeof window === "undefined") return;
|
||||
window.dispatchEvent(new CustomEvent(ASSETS_RESTORED_EVENT, { detail: payload }));
|
||||
}
|
||||
|
||||
export function emitDocumentsChanged(docId?: string) {
|
||||
if (typeof window === "undefined") return;
|
||||
window.dispatchEvent(new CustomEvent(DOCUMENTS_CHANGED_EVENT, { detail: { docId } }));
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser";
|
||||
|
||||
const buildReq = () =>
|
||||
new Request("https://example.com/api/test", {
|
||||
headers: {
|
||||
"x-forwarded-proto": "https",
|
||||
"x-forwarded-host": "app.example.com",
|
||||
},
|
||||
});
|
||||
|
||||
const base64UrlEncodeUtf8 = (input: string) => {
|
||||
return Buffer.from(input, "utf8")
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/g, "");
|
||||
};
|
||||
|
||||
describe("maybeProxyForBrowserUrl", () => {
|
||||
it("对 localhost URL 生成 proxy URL", () => {
|
||||
const req = buildReq();
|
||||
const raw = "http://localhost:3210/api/storage/xxx";
|
||||
const out = maybeProxyForBrowserUrl(req, raw);
|
||||
const expected = `https://app.example.com/api/onlyoffice/proxy?u=${base64UrlEncodeUtf8(raw)}`;
|
||||
expect(out).toBe(expected);
|
||||
});
|
||||
|
||||
it("对与 Convex Origin host 匹配的 URL 生成 proxy URL", () => {
|
||||
process.env.CONVEX_SELF_HOSTED_URL = "http://backend:3210";
|
||||
const req = buildReq();
|
||||
const raw = "http://backend:3210/api/storage/yyy";
|
||||
const out = maybeProxyForBrowserUrl(req, raw);
|
||||
const expected = `https://app.example.com/api/onlyoffice/proxy?u=${base64UrlEncodeUtf8(raw)}`;
|
||||
expect(out).toBe(expected);
|
||||
});
|
||||
|
||||
it("对外部 URL 不做处理", () => {
|
||||
process.env.CONVEX_SELF_HOSTED_URL = "http://backend:3210";
|
||||
const req = buildReq();
|
||||
const raw = "https://cdn.example.com/image.png";
|
||||
const out = maybeProxyForBrowserUrl(req, raw);
|
||||
expect(out).toBe(raw);
|
||||
});
|
||||
|
||||
it("已经是 /api/onlyoffice/proxy 的 URL 不重复包裹", () => {
|
||||
const req = buildReq();
|
||||
const raw = "/api/onlyoffice/proxy?u=abc";
|
||||
const out = maybeProxyForBrowserUrl(req, raw);
|
||||
expect(out).toBe(raw);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
const base64UrlEncodeUtf8 = (input: string) => {
|
||||
return Buffer.from(input, "utf8")
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/g, "");
|
||||
};
|
||||
|
||||
const getRequestOrigin = (request: Request) => {
|
||||
const xfProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
|
||||
const xfHost = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
|
||||
const host = xfHost || request.headers.get("host") || new URL(request.url).host;
|
||||
const protocol = (xfProto || new URL(request.url).protocol.replace(/:$/, "")) + ":";
|
||||
return `${protocol}//${host}`;
|
||||
};
|
||||
|
||||
const isLocalHostname = (hostname: string) =>
|
||||
hostname === "127.0.0.1" ||
|
||||
hostname === "localhost" ||
|
||||
hostname === "host.docker.internal" ||
|
||||
hostname === "0.0.0.0";
|
||||
|
||||
const getConvexOriginHost = () => {
|
||||
const raw = (process.env.CONVEX_SELF_HOSTED_URL ?? process.env.NEXT_PUBLIC_CONVEX_URL ?? "").trim();
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const u = new URL(raw);
|
||||
return u.hostname;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将“浏览器不可达”的内部回源 URL 包一层 `/api/onlyoffice/proxy?u=...`,避免前端直接请求内网地址。
|
||||
* 说明:该函数只做代理 URL 的生成;真正的 SSRF 防护在 `/api/onlyoffice/proxy` 内完成。
|
||||
*/
|
||||
export const maybeProxyForBrowserUrl = (request: Request, rawUrl: string) => {
|
||||
const input = String(rawUrl || "").trim();
|
||||
if (!input) return input;
|
||||
if (input.startsWith("/api/onlyoffice/proxy")) return input;
|
||||
|
||||
try {
|
||||
const u = new URL(input);
|
||||
const convexHost = getConvexOriginHost();
|
||||
const shouldProxy = isLocalHostname(u.hostname) || (convexHost ? u.hostname === convexHost : false);
|
||||
if (!shouldProxy) return input;
|
||||
|
||||
const origin = getRequestOrigin(request);
|
||||
const proxy = new URL("/api/onlyoffice/proxy", origin);
|
||||
proxy.searchParams.set("u", base64UrlEncodeUtf8(input));
|
||||
return proxy.toString();
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user