实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -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) {
|
||||
|
||||
在新工单中引用
屏蔽一个用户