实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-31 18:57:34 +08:00
父节点 e123d50f86
当前提交 410d8cd71b
共修改 23 个文件,包含 1352 行新增和 69 行删除
+47
查看文件
@@ -16,6 +16,10 @@ const {
autoBindExistingWecomEmployee,
fetchWecomLoginIdentity,
} = require("../services/integrations/wecomLoginAutoBinding");
const {
getWecomJsApiTicket,
signWecomJsSdkUrl,
} = require("../services/integrations/wecomJsSdk");
const BIND_COOKIE_NAME = "wecom_bind";
const PENDING_LOGIN_BIND_COOKIE_NAME = "wecom_login_bind";
@@ -34,6 +38,7 @@ const QUERY_SCHEMA = z.object({
deviceFingerprint: z.string().trim().min(1).max(128).optional(),
});
const BIND_BODY_SCHEMA = z.object({ returnTo: z.string().optional() }).strict().optional();
const JS_SDK_SIGNATURE_BODY_SCHEMA = z.object({ url: z.string().trim().url().max(2048) }).strict();
function refreshCookieOptions(config) {
return {
@@ -108,6 +113,23 @@ function resolvePrimaryAppOrigin(config, request) {
return `${protocol}://${host}`;
}
function normalizeJsSdkPageUrl(rawUrl, config, request) {
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { url: "invalid" } });
}
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.hash) {
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { url: "invalid" } });
}
const allowedOrigin = new URL(resolvePrimaryAppOrigin(config, request)).origin;
if (parsed.origin !== allowedOrigin) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 });
}
return parsed.href;
}
function parseAllowedReturnHosts(config, request) {
const hosts = new Set();
const appOrigins = String(config.appOrigin || "")
@@ -465,6 +487,31 @@ async function wecomAuthRoutes(fastify) {
return sendData(reply, request, resolveWecomEntryConfig(fastify.config));
});
fastify.post("/wecom/js-sdk-signature", { preHandler: fastify.authenticate }, async (request, reply) => {
const parsed = JS_SDK_SIGNATURE_BODY_SCHEMA.safeParse(request.body ?? {});
if (!parsed.success) {
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { url: "invalid" } });
}
const pageUrl = normalizeJsSdkPageUrl(parsed.data.url, fastify.config, request);
const ticketResult = await getWecomJsApiTicket({
config: fastify.config,
timeoutMs: fastify.config.wecomRequestTimeoutMs,
});
if (!ticketResult.ok) {
request.log.warn({ errorCode: ticketResult.code }, "wecom jsapi ticket unavailable");
throw new AppError({
code: "SYS_001",
message: "企业微信文件预览暂时不可用",
statusCode: 503,
details: { wecom: "jsapi_ticket_unavailable" },
});
}
return sendData(reply, request, {
corpId: String(fastify.config.wecomCorpId ?? ""),
...signWecomJsSdkUrl({ ticket: ticketResult.ticket, url: pageUrl }),
});
});
fastify.get("/wecom/login", async (request, reply) => {
const parsed = QUERY_SCHEMA.safeParse(request.query ?? {});
if (!parsed.success) {