R18-1/R18-3: 接入企业微信入口绑定与免登回退

这个提交包含在:
2026-03-22 08:33:53 +08:00
父节点 2a1ffe66f3
当前提交 215e3967bc
共修改 15 个文件,包含 1394 行新增和 26 行删除
+8
查看文件
@@ -16,6 +16,7 @@ const authPlugin = require("./plugins/auth");
const prismaPlugin = require("./plugins/prisma");
const swaggerDocsPlugin = require("./plugins/swagger");
const authRoutes = require("./routes/auth");
const wecomAuthRoutes = require("./routes/wecomAuth");
const meRoutes = require("./routes/me");
const reimbursementRoutes = require("./routes/reimbursements");
const invoicePoolRoutes = require("./routes/invoicePool");
@@ -52,6 +53,9 @@ const systemWorkspaceRoutes = require("./routes/systemWorkspace");
const presenceRoutes = require("./routes/presence");
const weeklyReportsRoutes = require("./routes/weeklyReports");
const todosRoutes = require("./routes/todos");
const projectsRoutes = require("./routes/projects");
const contractsRegistryRoutes = require("./routes/contractsRegistry");
const confidentialDocRefsRoutes = require("./routes/confidentialDocRefs");
const purchaseRequestRoutes = require("./routes/purchaseRequests");
const assetsRoutes = require("./routes/assets");
const assetMovementsRoutes = require("./routes/assetMovements");
@@ -297,6 +301,7 @@ async function buildApp() {
}
await app.register(authRoutes, { prefix: "/api/auth" });
await app.register(wecomAuthRoutes, { prefix: "/api/auth" });
await app.register(meRoutes, { prefix: "/api" });
await app.register(reimbursementRoutes, { prefix: "/api" });
await app.register(invoicePoolRoutes, { prefix: "/api" });
@@ -332,6 +337,9 @@ async function buildApp() {
await app.register(presenceRoutes, { prefix: "/api" });
await app.register(weeklyReportsRoutes, { prefix: "/api" });
await app.register(todosRoutes, { prefix: "/api" });
await app.register(projectsRoutes, { prefix: "/api" });
await app.register(contractsRegistryRoutes, { prefix: "/api" });
await app.register(confidentialDocRefsRoutes, { prefix: "/api" });
await app.register(purchaseRequestRoutes, { prefix: "/api" });
await app.register(assetsRoutes, { prefix: "/api" });
await app.register(assetMovementsRoutes, { prefix: "/api" });
+13
查看文件
@@ -56,6 +56,14 @@ function getConfig() {
oidcRedirectUri: process.env.OIDC_REDIRECT_URI ?? "",
oidcScopes: process.env.OIDC_SCOPES ?? "openid profile email",
wecomEnabled: process.env.WECOM_ENABLED === "true",
wecomCorpId: process.env.WECOM_CORP_ID ?? "",
wecomAgentId: process.env.WECOM_AGENT_ID ?? "",
wecomAgentSecret: process.env.WECOM_AGENT_SECRET ?? "",
wecomOauthEnabled: process.env.WECOM_OAUTH_ENABLED === "true",
wecomRedirectUri: process.env.WECOM_REDIRECT_URI ?? "",
wecomAllowedReturnHosts: process.env.WECOM_ALLOWED_RETURN_HOSTS ?? "",
appTimezone: process.env.APP_TIMEZONE ?? "Asia/Shanghai",
storageDir: path.resolve(serverRoot, storageDir),
storageDriver: process.env.STORAGE_DRIVER ?? "local",
@@ -115,6 +123,11 @@ function getConfig() {
weeklyReportEnabled: process.env.WEEKLY_REPORT_ENABLED === "true",
employeeLifecycleEnabled: process.env.EMPLOYEE_LIFECYCLE_ENABLED === "true",
todoBoardEnabled: process.env.TODO_BOARD_ENABLED === "true",
wecomEntryEnabled: process.env.WECOM_ENTRY_ENABLED === "true",
projectDomainEnabled: process.env.PROJECT_DOMAIN_ENABLED === "true",
projectLinkingEnabled: process.env.PROJECT_LINKING_ENABLED === "true",
confidentialDocRefsEnabled: process.env.CONFIDENTIAL_DOC_REFS_ENABLED === "true",
publicSecurityHardeningEnabled: process.env.PUBLIC_SECURITY_HARDENING_ENABLED === "true",
aiAssistEnabled: process.env.AI_ASSIST_ENABLED === "true",
aiProvider: process.env.AI_PROVIDER ?? "mock",
+15
查看文件
@@ -29,6 +29,7 @@ const TRAVEL_SELF_SERVICE_PERMISSIONS = [
];
const ASSET_SELF_SERVICE_PERMISSIONS = ["asset.view_own"];
const PROJECT_SELF_SERVICE_PERMISSIONS = ["project.view_own"];
const MASTERDATA_VIEW_PERMISSIONS = ["masterdata.view"];
const MASTERDATA_MANAGE_PERMISSIONS = ["masterdata.manage"];
@@ -37,6 +38,7 @@ const EMPLOYEE_BASE_PERMISSIONS = [
...EXPENSE_SELF_SERVICE_PERMISSIONS,
...LEAVE_SELF_SERVICE_PERMISSIONS,
...TRAVEL_SELF_SERVICE_PERMISSIONS,
...PROJECT_SELF_SERVICE_PERMISSIONS,
];
const ENTITY_PERMISSION_MAP = {
@@ -102,6 +104,11 @@ const ENTITY_PERMISSION_MAP = {
approve: "travel.approve",
manage: "travel.manage",
},
project: {
view_own: "project.view_own",
view_all: "project.view_all",
manage: "project.manage",
},
};
const ROLE_PERMISSIONS = {
@@ -123,8 +130,13 @@ const ROLE_PERMISSIONS = {
"travel.view_all",
"asset.view_all",
"report.dashboard.view",
"project.view_all",
"budget.view",
"project.view_all",
"project.manage",
"budget.manage",
"project.view_all",
"project.manage",
],
ceo: [
...EMPLOYEE_BASE_PERMISSIONS,
@@ -187,6 +199,7 @@ const ROLE_PERMISSIONS = {
"system.audit.view",
"payment.batch.manage",
"risk.review.manage",
"project.view_all",
],
administration: [
"expense.proxy_submit",
@@ -199,6 +212,8 @@ const ROLE_PERMISSIONS = {
"asset.view_all",
"asset.manage",
"asset.transfer",
"project.view_all",
"project.manage",
],
hr: [
...LEAVE_SELF_SERVICE_PERMISSIONS,
+82 -4
查看文件
@@ -29,6 +29,85 @@ function getRequestOrigin(request) {
}
}
function normalizeOriginValue(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
try {
return new URL(trimmed).origin.replace(/\/+$/, "");
} catch {
return null;
}
}
function parseAllowedOriginContext(config, request) {
const allowedOrigins = new Set();
const allowedHosts = new Set();
for (const item of String(config.appOrigin || "").split(",").map((value) => value.trim()).filter(Boolean)) {
const normalized = normalizeOriginValue(item);
if (!normalized) {
continue;
}
allowedOrigins.add(normalized);
try {
allowedHosts.add(new URL(normalized).host);
} catch {
// ignore invalid app origin host
}
}
for (const item of String(config.wecomAllowedReturnHosts || "").split(",").map((value) => value.trim()).filter(Boolean)) {
allowedHosts.add(item);
}
if (!allowedOrigins.size) {
const fallbackOrigin = normalizeOriginValue(resolvePrimaryAppOrigin(config, request));
if (fallbackOrigin) {
allowedOrigins.add(fallbackOrigin);
try {
allowedHosts.add(new URL(fallbackOrigin).host);
} catch {
// ignore
}
}
}
return { allowedOrigins, allowedHosts };
}
function isRequestOriginAllowed(config, request) {
const requestOrigin = getRequestOrigin(request);
if (!requestOrigin) {
return { allowed: true, requestOrigin: null, expectedOrigins: [] };
}
const normalizedRequestOrigin = normalizeOriginValue(requestOrigin);
if (!normalizedRequestOrigin) {
return { allowed: false, requestOrigin, expectedOrigins: [] };
}
const { allowedOrigins, allowedHosts } = parseAllowedOriginContext(config, request);
if (allowedOrigins.has(normalizedRequestOrigin)) {
return { allowed: true, requestOrigin: normalizedRequestOrigin, expectedOrigins: Array.from(allowedOrigins) };
}
try {
const host = new URL(normalizedRequestOrigin).host;
if (allowedHosts.has(host)) {
return { allowed: true, requestOrigin: normalizedRequestOrigin, expectedOrigins: Array.from(allowedOrigins) };
}
} catch {
// ignore invalid host
}
return { allowed: false, requestOrigin: normalizedRequestOrigin, expectedOrigins: Array.from(allowedOrigins) };
}
function refreshCookieOptions(config) {
return {
httpOnly: true,
@@ -361,16 +440,15 @@ async function authRoutes(fastify) {
});
}
const expectedOrigin = fastify.config.appOrigin;
const requestOrigin = getRequestOrigin(request);
if (expectedOrigin && requestOrigin && requestOrigin !== expectedOrigin) {
const originCheck = isRequestOriginAllowed(fastify.config, request);
if (!originCheck.allowed) {
await fastify.prisma.auditLog.create({
data: buildAuditLogData(request, {
actorId: session.employee_id,
action: "AUTH_REFRESH_ORIGIN_MISMATCH",
targetType: "employee",
targetId: session.employee_id,
extra: { expectedOrigin, requestOrigin },
extra: { expectedOrigins: originCheck.expectedOrigins, requestOrigin: originCheck.requestOrigin },
}),
});
throw new AppError({
+2
查看文件
@@ -19,6 +19,7 @@ async function meRoutes(fastify) {
direct_manager_id: true,
status: true,
roles: { select: { role_code: true } },
wecom_identity_binding: { select: { external_user_id: true, created_at: true } },
},
});
@@ -33,6 +34,7 @@ async function meRoutes(fastify) {
directManagerId: employee?.direct_manager_id ?? null,
status: employee?.status ?? "active",
roleCodes,
wecomBinding: employee?.wecom_identity_binding ? { externalUserId: employee.wecom_identity_binding.external_user_id, createdAt: employee.wecom_identity_binding.created_at?.toISOString?.() ?? null } : null,
});
},
);
+402
查看文件
@@ -0,0 +1,402 @@
const crypto = require("node:crypto");
const { z } = require("zod");
const { AppError } = require("../utils/AppError");
const { buildAuditLogData } = require("../utils/audit");
const { randomRefreshToken, sha256Hex } = require("../utils/crypto");
const { signPayload, verifyToken } = require("../utils/signedUrl");
const { sendData } = require("../utils/reply");
const BIND_COOKIE_NAME = "wecom_bind";
const DATE_WINDOW_MS = 10 * 60 * 1000;
const QUERY_SCHEMA = z.object({
returnTo: z.string().optional(),
mode: z.enum(["login", "bind"]).optional(),
});
const BIND_BODY_SCHEMA = z.object({ returnTo: z.string().optional() }).strict().optional();
function refreshCookieOptions(config) {
return {
httpOnly: true,
sameSite: "strict",
secure: config.cookieSecure,
path: "/api/auth",
maxAge: config.refreshTtlSeconds,
};
}
function bindCookieOptions(config) {
return {
httpOnly: true,
sameSite: "lax",
secure: config.cookieSecure,
path: "/api/auth/wecom",
maxAge: DATE_WINDOW_MS / 1000,
};
}
function resolvePrimaryAppOrigin(config, request) {
const configured = typeof config.appOrigin === "string" ? config.appOrigin : "";
const first = configured
.split(",")
.map((s) => s.trim())
.filter(Boolean)[0];
if (first) return first;
const host = request.headers.host ?? "127.0.0.1:3000";
const protocol = request.protocol ?? "http";
return `${protocol}://${host}`;
}
function parseAllowedReturnHosts(config, request) {
const hosts = new Set();
const appOrigins = String(config.appOrigin || "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
for (const item of appOrigins) {
try {
hosts.add(new URL(item).host);
} catch {
// ignore invalid origin
}
}
for (const item of String(config.wecomAllowedReturnHosts || "").split(",").map((v) => v.trim()).filter(Boolean)) {
hosts.add(item);
}
if (!hosts.size) {
try {
hosts.add(new URL(resolvePrimaryAppOrigin(config, request)).host);
} catch {
// ignore
}
}
return hosts;
}
function sanitizeReturnTo(rawValue, config, request) {
if (typeof rawValue !== "string") {
return "/portal";
}
const trimmed = rawValue.trim();
if (!trimmed) {
return "/portal";
}
if (trimmed.startsWith("/")) {
return trimmed.startsWith("//") ? "/portal" : trimmed;
}
try {
const url = new URL(trimmed);
const allowedHosts = parseAllowedReturnHosts(config, request);
if (!allowedHosts.has(url.host)) {
return "/portal";
}
return `${url.pathname || "/portal"}${url.search || ""}${url.hash || ""}`;
} catch {
return "/portal";
}
}
function assertWecomEnabled(config) {
if (!config.wecomEnabled || !config.wecomOauthEnabled) {
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
}
if (!config.wecomCorpId || !config.wecomAgentId || !config.wecomRedirectUri) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { wecom: "missing_required_config" },
});
}
}
async function getWecomAccessToken(config) {
if (!config.wecomAgentSecret) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { wecom: "missing_agent_secret" },
});
}
const url = new URL("https://qyapi.weixin.qq.com/cgi-bin/gettoken");
url.searchParams.set("corpid", config.wecomCorpId);
url.searchParams.set("corpsecret", config.wecomAgentSecret);
const response = await fetch(url);
const payload = await response.json().catch(() => null);
if (!response.ok || !payload || Number(payload.errcode ?? 0) !== 0 || !payload.access_token) {
throw new AppError({
code: "AUTH_003",
message: "企业微信认证失败",
statusCode: 401,
details: { wecom: "token_exchange_failed", payload },
});
}
return String(payload.access_token);
}
async function exchangeWecomCode(config, code) {
const accessToken = await getWecomAccessToken(config);
const url = new URL("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo");
url.searchParams.set("access_token", accessToken);
url.searchParams.set("code", code);
const response = await fetch(url);
const payload = await response.json().catch(() => null);
if (!response.ok || !payload || Number(payload.errcode ?? 0) !== 0) {
throw new AppError({
code: "AUTH_003",
message: "企业微信认证失败",
statusCode: 401,
details: { wecom: "userinfo_exchange_failed", payload },
});
}
return {
externalUserId: typeof payload.UserId === "string" && payload.UserId.trim() ? payload.UserId.trim() : null,
externalOpenUserId: typeof payload.OpenId === "string" && payload.OpenId.trim() ? payload.OpenId.trim() : null,
deviceId: typeof payload.DeviceId === "string" ? payload.DeviceId : null,
};
}
function buildWecomAuthorizeUrl(config, state) {
const redirectUri = encodeURIComponent(config.wecomRedirectUri);
const scope = "snsapi_base";
return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${encodeURIComponent(config.wecomCorpId)}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&agentid=${encodeURIComponent(config.wecomAgentId)}&state=${encodeURIComponent(state)}#wechat_redirect`;
}
async function createAuthenticatedSession({ prisma, request, config, employeeId, tokenVersion, deviceFingerprint, auditAction, extra = {} }) {
const refreshToken = randomRefreshToken();
const refreshTokenHash = sha256Hex(refreshToken);
const now = new Date();
const expiresAt = new Date(now.getTime() + config.refreshTtlSeconds * 1000);
await prisma.$transaction(async (tx) => {
const activeSessions = await tx.authSession.findMany({
where: {
employee_id: employeeId,
revoked_at: null,
expires_at: { gt: now },
},
orderBy: { created_at: "asc" },
select: { id: true },
});
const maxSessions = 5;
const allowedExisting = maxSessions - 1;
if (activeSessions.length > allowedExisting) {
const revokeCount = activeSessions.length - allowedExisting;
const idsToRevoke = activeSessions.slice(0, revokeCount).map((s) => s.id);
await tx.authSession.updateMany({ where: { id: { in: idsToRevoke } }, data: { revoked_at: now } });
}
await tx.authSession.create({
data: {
employee_id: employeeId,
refresh_token_hash: refreshTokenHash,
device_fingerprint: deviceFingerprint,
ip_address: request.ip,
user_agent: String(request.headers["user-agent"] ?? ""),
expires_at: expiresAt,
},
});
await tx.auditLog.create({
data: buildAuditLogData(request, {
actorId: employeeId,
action: auditAction,
targetType: "employee",
targetId: employeeId,
extra,
}),
});
});
return {
refreshToken,
accessToken: request.server.issueAccessToken({ id: employeeId, token_version: tokenVersion }),
};
}
async function wecomAuthRoutes(fastify) {
fastify.get("/wecom/config", async (request, reply) => {
const enabled = Boolean(fastify.config.wecomEnabled && fastify.config.wecomOauthEnabled && fastify.config.wecomCorpId && fastify.config.wecomAgentId && fastify.config.wecomRedirectUri);
return sendData(reply, request, {
enabled,
corpId: enabled ? fastify.config.wecomCorpId : "",
agentId: enabled ? fastify.config.wecomAgentId : "",
oauthEnabled: enabled,
loginUrl: enabled ? "/api/auth/wecom/login?returnTo=%2Fportal" : null,
});
});
fastify.get("/wecom/login", async (request, reply) => {
assertWecomEnabled(fastify.config);
const parsed = QUERY_SCHEMA.safeParse(request.query ?? {});
if (!parsed.success) {
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: parsed.error.flatten() });
}
const mode = parsed.data.mode ?? "login";
if (mode === "bind") {
const bindCookie = request.cookies?.[BIND_COOKIE_NAME];
const bindState = verifyToken(bindCookie, fastify.config.jwtSecret);
if (!bindState.ok) {
reply.clearCookie(BIND_COOKIE_NAME, bindCookieOptions(fastify.config));
throw new AppError({ code: "AUTH_004", message: "Access Token 已过期/无效", statusCode: 401 });
}
}
const returnTo = sanitizeReturnTo(parsed.data.returnTo, fastify.config, request);
const state = signPayload({
exp: Date.now() + DATE_WINDOW_MS,
nonce: crypto.randomUUID(),
returnTo,
mode,
}, fastify.config.jwtSecret);
return reply.redirect(buildWecomAuthorizeUrl(fastify.config, state));
});
fastify.post("/wecom/bind", { preHandler: fastify.authenticate }, async (request, reply) => {
assertWecomEnabled(fastify.config);
const parsed = BIND_BODY_SCHEMA.safeParse(request.body ?? {});
if (!parsed.success) {
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: parsed.error.flatten() });
}
const returnTo = sanitizeReturnTo(parsed.data?.returnTo, fastify.config, request);
const token = signPayload({
exp: Date.now() + DATE_WINDOW_MS,
employeeId: request.employee.id,
returnTo,
}, fastify.config.jwtSecret);
reply.setCookie(BIND_COOKIE_NAME, token, bindCookieOptions(fastify.config));
return sendData(reply, request, {
loginUrl: `/api/auth/wecom/login?mode=bind&returnTo=${encodeURIComponent(returnTo)}`,
});
});
fastify.post("/wecom/unbind", { preHandler: fastify.authenticate }, async (request, reply) => {
await fastify.prisma.wecomIdentityBinding.deleteMany({ where: { employee_id: request.employee.id } });
try {
await fastify.prisma.auditLog.create({
data: buildAuditLogData(request, {
actorId: request.employee.id,
action: "AUTH_WECOM_UNBIND",
targetType: "employee",
targetId: request.employee.id,
extra: {},
}),
});
} catch (error) {
request.log.error({ err: error }, "wecom unbind audit failed");
}
return sendData(reply, request, { ok: true });
});
fastify.get("/wecom/callback", async (request, reply) => {
assertWecomEnabled(fastify.config);
const stateToken = String(request.query?.state ?? "").trim();
const code = String(request.query?.code ?? "").trim();
const verified = verifyToken(stateToken, fastify.config.jwtSecret);
if (!verified.ok || !code) {
throw new AppError({ code: "AUTH_004", message: "Access Token 已过期/无效", statusCode: 401 });
}
const ctx = verified.payload ?? {};
if (typeof ctx.exp !== "number" || ctx.exp <= Date.now()) {
throw new AppError({ code: "AUTH_004", message: "Access Token 已过期/无效", statusCode: 401 });
}
const mode = ctx.mode === "bind" ? "bind" : "login";
const returnTo = sanitizeReturnTo(ctx.returnTo, fastify.config, request);
const profile = await exchangeWecomCode(fastify.config, code);
if (!profile.externalUserId) {
throw new AppError({
code: "AUTH_003",
message: "企业微信认证失败",
statusCode: 401,
details: { wecom: "missing_user_id" },
});
}
const origin = resolvePrimaryAppOrigin(fastify.config, request);
if (mode === "bind") {
const bindCookie = request.cookies?.[BIND_COOKIE_NAME];
const bindVerified = verifyToken(bindCookie, fastify.config.jwtSecret);
if (!bindVerified.ok) {
reply.clearCookie(BIND_COOKIE_NAME, bindCookieOptions(fastify.config));
return reply.redirect(`${origin}${returnTo}?wecomBind=expired`);
}
const bindPayload = bindVerified.payload ?? {};
if (typeof bindPayload.employeeId !== "string") {
reply.clearCookie(BIND_COOKIE_NAME, bindCookieOptions(fastify.config));
return reply.redirect(`${origin}${returnTo}?wecomBind=invalid`);
}
const employee = await fastify.prisma.employee.findUnique({
where: { id: bindPayload.employeeId },
select: { id: true, status: true },
});
if (!employee || employee.status !== "active") {
reply.clearCookie(BIND_COOKIE_NAME, bindCookieOptions(fastify.config));
return reply.redirect(`${origin}${returnTo}?wecomBind=employee_inactive`);
}
const conflicting = await fastify.prisma.wecomIdentityBinding.findFirst({
where: { external_user_id: profile.externalUserId, NOT: { employee_id: employee.id } },
select: { employee_id: true },
});
if (conflicting) {
reply.clearCookie(BIND_COOKIE_NAME, bindCookieOptions(fastify.config));
return reply.redirect(`${origin}${returnTo}?wecomBind=conflict`);
}
await fastify.prisma.wecomIdentityBinding.upsert({
where: { employee_id: employee.id },
update: {
external_user_id: profile.externalUserId,
external_open_user_id: profile.externalOpenUserId,
updated_by: employee.id,
},
create: {
employee_id: employee.id,
external_user_id: profile.externalUserId,
external_open_user_id: profile.externalOpenUserId,
created_by: employee.id,
updated_by: employee.id,
},
});
await fastify.prisma.auditLog.create({
data: buildAuditLogData(request, {
actorId: employee.id,
action: "AUTH_WECOM_BIND",
targetType: "employee",
targetId: employee.id,
extra: { externalUserId: profile.externalUserId },
}),
});
reply.clearCookie(BIND_COOKIE_NAME, bindCookieOptions(fastify.config));
return reply.redirect(`${origin}${returnTo}?wecomBind=success`);
}
const binding = await fastify.prisma.wecomIdentityBinding.findUnique({
where: { external_user_id: profile.externalUserId },
select: { employee_id: true, employee: { select: { id: true, status: true, token_version: true } } },
});
if (!binding?.employee || binding.employee.status !== "active") {
return reply.redirect(`${origin}/login?redirect=${encodeURIComponent(returnTo)}&wecom=unbound`);
}
const deviceFingerprint = `wecom:${profile.externalUserId}`;
const session = await createAuthenticatedSession({
prisma: fastify.prisma,
request,
config: fastify.config,
employeeId: binding.employee.id,
tokenVersion: binding.employee.token_version,
deviceFingerprint,
auditAction: "AUTH_WECOM_LOGIN_SUCCESS",
extra: { externalUserId: profile.externalUserId },
});
reply.setCookie(fastify.config.refreshCookieName, session.refreshToken, refreshCookieOptions(fastify.config));
return reply.redirect(`${origin}${returnTo}`);
});
}
module.exports = wecomAuthRoutes;
+35
查看文件
@@ -310,6 +310,41 @@ const DEFAULT_FEATURE_FLAGS = [
department_ids: [],
note: "bootstrap default: force_off",
},
{
flag_key: "wecom_entry_v1",
override_mode: "force_off",
rollout_type: "global",
department_ids: [],
note: "bootstrap default: force_off",
},
{
flag_key: "project_domain_v1",
override_mode: "force_off",
rollout_type: "global",
department_ids: [],
note: "bootstrap default: force_off",
},
{
flag_key: "project_linking_v1",
override_mode: "force_off",
rollout_type: "global",
department_ids: [],
note: "bootstrap default: force_off",
},
{
flag_key: "confidential_doc_refs_v1",
override_mode: "force_off",
rollout_type: "global",
department_ids: [],
note: "bootstrap default: force_off",
},
{
flag_key: "public_security_hardening_v1",
override_mode: "force_off",
rollout_type: "global",
department_ids: [],
note: "bootstrap default: force_off",
},
];
const DEFAULT_BUDGET_CONTROL_RULES = [
+25
查看文件
@@ -14,6 +14,11 @@ const FEATURE_FLAG_KEYS = {
weeklyReport: "weekly_report_v1",
employeeLifecycle: "employee_lifecycle_v1",
todoBoard: "todo_board_v1",
wecomEntry: "wecom_entry_v1",
projectDomain: "project_domain_v1",
projectLinking: "project_linking_v1",
confidentialDocRefs: "confidential_doc_refs_v1",
publicSecurityHardening: "public_security_hardening_v1",
};
const FEATURE_FLAG_KEY_LIST = Object.freeze(Object.values(FEATURE_FLAG_KEYS));
@@ -127,6 +132,26 @@ function resolveEnvGate(config, flagKey) {
return Boolean(config.todoBoardEnabled);
}
if (flagKey === FEATURE_FLAG_KEYS.wecomEntry) {
return Boolean(config.wecomEntryEnabled);
}
if (flagKey === FEATURE_FLAG_KEYS.projectDomain) {
return Boolean(config.projectDomainEnabled);
}
if (flagKey === FEATURE_FLAG_KEYS.projectLinking) {
return Boolean(config.projectLinkingEnabled);
}
if (flagKey === FEATURE_FLAG_KEYS.confidentialDocRefs) {
return Boolean(config.confidentialDocRefsEnabled);
}
if (flagKey === FEATURE_FLAG_KEYS.publicSecurityHardening) {
return Boolean(config.publicSecurityHardeningEnabled);
}
return false;
}