feat: 负责人下钻视图+项目负责人编辑管理+企微授权自助绑定
- 任务/项目负责人 union scope:任务中心分组视图、工作台项目进度卡、收录验证列 - 项目负责人可管理自己项目:编辑/删除项目、编辑任务、分配负责人、推送、项目内建任务;堵死转移归属/越权新建/任务迁移 - 只读依赖放开给已登录:inspect_feishu、wecom_list_internal_contacts - 企微 OAuth 自助绑定:/api/wecom-bind(state 签名校验+登录态一致性+归属冲突检查),未绑定 banner 增加入口 - 工作台项目进度卡与未绑定 banner 排版修复 - 修 user-auth 判空分支被 Turbopack dev 折叠删除导致匿名请求 500
This commit is contained in:
@@ -34,6 +34,10 @@ const mvpDb = readFileSync(
|
||||
new URL("../lib/mvp-db.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const ownerScope = readFileSync(
|
||||
new URL("../lib/owner-scope.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
function section(source, action) {
|
||||
const start = source.indexOf(`"${action}"`);
|
||||
@@ -44,9 +48,6 @@ function section(source, action) {
|
||||
|
||||
test("manager-only actions gate on owner scope", () => {
|
||||
const managerOnly = [
|
||||
"inspect_feishu",
|
||||
"create_task",
|
||||
"create_screenshot_task",
|
||||
"claim",
|
||||
"save_collection_schedule",
|
||||
"run_due_collections",
|
||||
@@ -57,18 +58,9 @@ test("manager-only actions gate on owner scope", () => {
|
||||
"send_test_wecom",
|
||||
"trigger_wecom_overdue_escalation",
|
||||
"wecom_list_external_contacts",
|
||||
"wecom_list_internal_contacts",
|
||||
"wecom_sync_group_chats",
|
||||
"project_list",
|
||||
"project_detail",
|
||||
"project_save",
|
||||
"project_delete",
|
||||
"project_ensure_main_chat",
|
||||
"task_set_owner",
|
||||
"task_update",
|
||||
"partner_save",
|
||||
"partner_group_owners",
|
||||
"project_push_task",
|
||||
"refresh_account_fields",
|
||||
"account_field_refresh_status",
|
||||
];
|
||||
@@ -82,6 +74,86 @@ test("manager-only actions gate on owner scope", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("project-owner management actions gate on project ownership", () => {
|
||||
for (const action of [
|
||||
"project_detail",
|
||||
"project_delete",
|
||||
"project_ensure_main_chat",
|
||||
"create_task",
|
||||
"create_screenshot_task",
|
||||
]) {
|
||||
const body = section(actionRoute, action);
|
||||
assert.match(
|
||||
body,
|
||||
/projectOwnerGate\(/,
|
||||
`${action} 必须校验项目负责人归属(owner 空 projectId 也 403)`,
|
||||
);
|
||||
}
|
||||
for (const action of ["task_set_owner", "task_update", "project_push_task"]) {
|
||||
const body = section(actionRoute, action);
|
||||
assert.match(
|
||||
body,
|
||||
/taskProjectOwnerGate\(/,
|
||||
`${action} 必须按任务→项目链校验项目负责人归属`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("project_save blocks creation and ownership transfer for project owners", () => {
|
||||
const body = section(actionRoute, "project_save");
|
||||
assert.match(body, /managerForbidden\(\)/, "项目负责人不可新建项目");
|
||||
assert.match(body, /projectOwnerGate\(/, "项目负责人只能保存自己负责的项目");
|
||||
assert.match(
|
||||
body,
|
||||
/SELECT owner_userid FROM projects WHERE id = \?/,
|
||||
"owner 路径必须回填 DB 现值,堵死 ownerUserid 转移",
|
||||
);
|
||||
assert.match(body, /nextOwnerUserid/);
|
||||
});
|
||||
|
||||
test("task_update blocks project migration by project owners", () => {
|
||||
const body = section(actionRoute, "task_update");
|
||||
assert.match(
|
||||
body,
|
||||
/projectOwnerGate\(nextProjectId\)/,
|
||||
"迁移目标项目也必须本人负责",
|
||||
);
|
||||
});
|
||||
|
||||
test("read-only dependencies open to any logged-in user", () => {
|
||||
for (const action of ["inspect_feishu", "wecom_list_internal_contacts"]) {
|
||||
const body = section(actionRoute, action);
|
||||
assert.ok(
|
||||
!/managerGate\(\)/.test(body),
|
||||
`${action} 不再是管理员专属(项目负责人建任务/选人依赖)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("owner-scope exposes strict project ownership helpers", () => {
|
||||
assert.match(ownerScope, /export async function isProjectOwner\(/);
|
||||
const projectOwner = ownerScope.slice(
|
||||
ownerScope.indexOf("export async function isProjectOwner"),
|
||||
ownerScope.indexOf("export async function isTaskProjectOwner"),
|
||||
);
|
||||
assert.match(
|
||||
projectOwner,
|
||||
/SELECT id FROM projects WHERE id = \? AND owner_userid = \? LIMIT 1/,
|
||||
);
|
||||
|
||||
assert.match(ownerScope, /export async function isTaskProjectOwner\(/);
|
||||
const taskProjectOwner = ownerScope.slice(
|
||||
ownerScope.indexOf("export async function isTaskProjectOwner"),
|
||||
ownerScope.indexOf("export function ownerForbidden"),
|
||||
);
|
||||
assert.match(taskProjectOwner, /JOIN projects p/);
|
||||
assert.ok(!/LEFT JOIN/.test(taskProjectOwner), "无项目归属的任务不放行");
|
||||
assert.ok(
|
||||
!/t\.owner_userid = \?/.test(taskProjectOwner),
|
||||
"任务负责人本人不因此获得编辑权",
|
||||
);
|
||||
});
|
||||
|
||||
test("collect actions stay manager-only", () => {
|
||||
const body = section(actionRoute, "collect_now");
|
||||
assert.match(body, /managerGate\(\)/);
|
||||
@@ -122,6 +194,30 @@ test("task_report_list scopes reports by owner and drops the broken gate", () =>
|
||||
);
|
||||
});
|
||||
|
||||
test("project_list opens to owner scope with server-side filtering", () => {
|
||||
const body = section(actionRoute, "project_list");
|
||||
assert.match(
|
||||
body,
|
||||
/listProjects\(db, ownerScopeUserid\(scope\)\)/,
|
||||
"project_list 按负责人范围过滤,不再 managerGate",
|
||||
);
|
||||
assert.ok(
|
||||
!/managerGate\(\)/.test(body),
|
||||
"project_list 已放开给项目负责人(只读)",
|
||||
);
|
||||
});
|
||||
|
||||
test("customer chat actions keep strict task-owner gate", () => {
|
||||
for (const action of ["task_customer_chats", "task_bind_customer_chat"]) {
|
||||
const body = section(actionRoute, action);
|
||||
assert.match(
|
||||
body,
|
||||
/strict: true/,
|
||||
`${action} 保持严格任务负责人门,项目负责人不可代绑`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("no unawaited isManagerRequest gates remain", () => {
|
||||
assert.ok(
|
||||
!actionRoute.includes("if (!isManagerRequest(request))"),
|
||||
@@ -181,11 +277,14 @@ test("getDashboardData filters tasks and distributions by owner", () => {
|
||||
mvpDb.indexOf("export async function getDashboardData"),
|
||||
mvpDb.indexOf("export type TaskReportRow"),
|
||||
);
|
||||
const occurrences = dashboard.match(/WHERE t\.owner_userid = \?/g) ?? [];
|
||||
const occurrences =
|
||||
dashboard.match(
|
||||
/WHERE \(t\.owner_userid = \? OR \(t\.project_id != '' AND pj\.owner_userid = \?\)\)/g,
|
||||
) ?? [];
|
||||
assert.equal(
|
||||
occurrences.length,
|
||||
2,
|
||||
"tasks 与 distributions 两条查询都要按 owner 过滤",
|
||||
"tasks 与 distributions 两条查询都要按 union scope 过滤(任务负责人 OR 项目负责人)",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user