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:
@@ -68,9 +68,12 @@ import { isManagerRequest, managerForbidden } from "../../../lib/user-auth";
|
||||
import {
|
||||
getOwnerScope,
|
||||
isDistributionOwner,
|
||||
isProjectOwner,
|
||||
isTaskOwner,
|
||||
isTaskProjectOwner,
|
||||
ownerForbidden,
|
||||
ownerScopeUserid,
|
||||
projectForbidden,
|
||||
} from "../../../lib/owner-scope";
|
||||
import { extractPublishUrl } from "../../../lib/publish-url";
|
||||
import {
|
||||
@@ -126,10 +129,11 @@ export async function POST(request: Request) {
|
||||
const scope = await getOwnerScope(request);
|
||||
const managerGate = () =>
|
||||
scope.kind === "owner" ? managerForbidden() : null;
|
||||
const taskOwnerGate = async (taskId: unknown) => {
|
||||
const taskOwnerGate = async (taskId: unknown, options?: { strict?: boolean }) => {
|
||||
if (scope.kind === "full") return null;
|
||||
const id = String(taskId ?? "").trim();
|
||||
return id && (await isTaskOwner(db, id, scope.wecomUserId))
|
||||
return id &&
|
||||
(await isTaskOwner(db, id, scope.wecomUserId, options))
|
||||
? null
|
||||
: ownerForbidden();
|
||||
};
|
||||
@@ -140,10 +144,22 @@ export async function POST(request: Request) {
|
||||
? null
|
||||
: ownerForbidden();
|
||||
};
|
||||
const projectOwnerGate = async (projectId: unknown) => {
|
||||
if (scope.kind === "full") return null;
|
||||
const id = String(projectId ?? "").trim();
|
||||
return id && (await isProjectOwner(db, id, scope.wecomUserId))
|
||||
? null
|
||||
: projectForbidden();
|
||||
};
|
||||
const taskProjectOwnerGate = async (taskId: unknown) => {
|
||||
if (scope.kind === "full") return null;
|
||||
const id = String(taskId ?? "").trim();
|
||||
return id && (await isTaskProjectOwner(db, id, scope.wecomUserId))
|
||||
? null
|
||||
: projectForbidden();
|
||||
};
|
||||
|
||||
if (body.action === "inspect_feishu") {
|
||||
const forbidden = managerGate();
|
||||
if (forbidden) return forbidden;
|
||||
const source = await readFeishuSource(
|
||||
String(body.feishuUrl ?? "").trim(),
|
||||
env as unknown as FeishuBindings,
|
||||
@@ -167,7 +183,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
if (body.action === "create_task") {
|
||||
const forbidden = managerGate();
|
||||
const forbidden = await projectOwnerGate(body.projectId);
|
||||
if (forbidden) return forbidden;
|
||||
const name = String(body.name ?? "").trim();
|
||||
const brand = String(body.brand ?? "").trim();
|
||||
@@ -195,15 +211,13 @@ export async function POST(request: Request) {
|
||||
);
|
||||
ownerWarning = await assignOwnerQuietly(creation.taskId, body.ownerUserid);
|
||||
} else if (body.action === "create_screenshot_task") {
|
||||
const forbidden = managerGate();
|
||||
const forbidden = await projectOwnerGate(body.projectId);
|
||||
if (forbidden) return forbidden;
|
||||
const creation = await createScreenshotTask({
|
||||
name: String(body.name ?? "").trim(),
|
||||
brand: String(body.brand ?? "").trim(),
|
||||
relatedTaskId: String(body.relatedTaskId ?? "").trim(),
|
||||
dueAt: String(body.dueAt ?? "").trim(),
|
||||
keyword: String(body.keyword ?? "").trim(),
|
||||
instructions: String(body.instructions ?? "").trim(),
|
||||
quantity: numberValue(body.quantity),
|
||||
exampleImageKey: String(body.exampleImageKey ?? "").trim(),
|
||||
projectId: String(body.projectId ?? "").trim(),
|
||||
ownerUserid: String(body.ownerUserid ?? "").trim(),
|
||||
@@ -765,8 +779,6 @@ export async function POST(request: Request) {
|
||||
const contacts = await listExternalContacts(wecomConfig);
|
||||
return Response.json({ contacts });
|
||||
} else if (body.action === "wecom_list_internal_contacts") {
|
||||
const forbidden = managerGate();
|
||||
if (forbidden) return forbidden;
|
||||
const wecomConfig = resolveWecomConfig(
|
||||
env as unknown as WecomBindings,
|
||||
);
|
||||
@@ -816,16 +828,27 @@ export async function POST(request: Request) {
|
||||
const pushes = await listGroupPushes(db, taskId || undefined);
|
||||
return Response.json({ pushes });
|
||||
} else if (body.action === "project_list") {
|
||||
const forbidden = managerGate();
|
||||
if (forbidden) return forbidden;
|
||||
return Response.json({ projects: await listProjects(db) });
|
||||
return Response.json({
|
||||
projects: await listProjects(db, ownerScopeUserid(scope)),
|
||||
});
|
||||
} else if (body.action === "project_detail") {
|
||||
const forbidden = managerGate();
|
||||
const forbidden = await projectOwnerGate(body.projectId);
|
||||
if (forbidden) return forbidden;
|
||||
return Response.json(await getProjectDetail(db, String(body.projectId ?? "")));
|
||||
} else if (body.action === "project_save") {
|
||||
const forbidden = managerGate();
|
||||
const projectId = String(body.id ?? "").trim();
|
||||
// 项目负责人只能更新自己负责的项目,且归属 ownerUserid 强制保留原值
|
||||
if (scope.kind === "owner" && !projectId) return managerForbidden();
|
||||
const forbidden = await projectOwnerGate(projectId);
|
||||
if (forbidden) return forbidden;
|
||||
let nextOwnerUserid = body.ownerUserid;
|
||||
if (scope.kind === "owner") {
|
||||
const current = await db
|
||||
.prepare("SELECT owner_userid FROM projects WHERE id = ?")
|
||||
.bind(projectId)
|
||||
.first<{ owner_userid: string }>();
|
||||
nextOwnerUserid = current?.owner_userid ?? "";
|
||||
}
|
||||
const saved = await saveProject(
|
||||
db,
|
||||
{
|
||||
@@ -833,19 +856,19 @@ export async function POST(request: Request) {
|
||||
name: body.name,
|
||||
brand: body.brand,
|
||||
mainGroupMemberIds: body.mainGroupMemberIds,
|
||||
ownerUserid: body.ownerUserid,
|
||||
ownerUserid: nextOwnerUserid,
|
||||
note: body.note,
|
||||
},
|
||||
resolveWecomConfig(env as unknown as WecomBindings),
|
||||
);
|
||||
return Response.json(saved);
|
||||
} else if (body.action === "project_delete") {
|
||||
const forbidden = managerGate();
|
||||
const forbidden = await projectOwnerGate(body.projectId);
|
||||
if (forbidden) return forbidden;
|
||||
await deleteProject(db, String(body.projectId ?? ""));
|
||||
return Response.json({ ok: true });
|
||||
} else if (body.action === "project_ensure_main_chat") {
|
||||
const forbidden = managerGate();
|
||||
const forbidden = await projectOwnerGate(body.projectId);
|
||||
if (forbidden) return forbidden;
|
||||
const result = await ensureProjectMainChat(
|
||||
db,
|
||||
@@ -854,7 +877,7 @@ export async function POST(request: Request) {
|
||||
);
|
||||
return Response.json(result);
|
||||
} else if (body.action === "task_set_owner") {
|
||||
const forbidden = managerGate();
|
||||
const forbidden = await taskProjectOwnerGate(body.taskId);
|
||||
if (forbidden) return forbidden;
|
||||
const taskId = String(body.taskId ?? "").trim();
|
||||
const ownerUserid = String(body.ownerUserid ?? "").trim();
|
||||
@@ -882,8 +905,16 @@ export async function POST(request: Request) {
|
||||
.first<{ owner_userid: string; task_chat_id: string }>();
|
||||
return Response.json({ ok: true, assignment, task });
|
||||
} else if (body.action === "task_update") {
|
||||
const forbidden = managerGate();
|
||||
const forbidden = await taskProjectOwnerGate(body.taskId);
|
||||
if (forbidden) return forbidden;
|
||||
if (scope.kind === "owner") {
|
||||
// 迁移目标也必须是本人负责的项目;置空(脱离项目)同样拒绝
|
||||
const nextProjectId = String(body.projectId ?? "").trim();
|
||||
const projectForbiddenResponse = nextProjectId
|
||||
? await projectOwnerGate(nextProjectId)
|
||||
: projectForbidden();
|
||||
if (projectForbiddenResponse) return projectForbiddenResponse;
|
||||
}
|
||||
const platform = body.platform === "抖音" || body.platform === "小红书"
|
||||
? (body.platform as "小红书" | "抖音")
|
||||
: undefined;
|
||||
@@ -919,7 +950,7 @@ export async function POST(request: Request) {
|
||||
if (forbidden) return forbidden;
|
||||
return Response.json({ partners: await listGroupOwnerPartners(db) });
|
||||
} else if (body.action === "project_push_task") {
|
||||
const forbidden = managerGate();
|
||||
const forbidden = await taskProjectOwnerGate(body.taskId);
|
||||
if (forbidden) return forbidden;
|
||||
const wecomConfig = resolveWecomConfig(env as unknown as WecomBindings);
|
||||
const outcome = await pushProjectTask(
|
||||
@@ -1008,7 +1039,7 @@ export async function POST(request: Request) {
|
||||
});
|
||||
} else if (body.action === "task_customer_chats") {
|
||||
const taskId = String(body.taskId ?? "").trim();
|
||||
const forbidden = await taskOwnerGate(taskId);
|
||||
const forbidden = await taskOwnerGate(taskId, { strict: true });
|
||||
if (forbidden) return forbidden;
|
||||
const task = await db
|
||||
.prepare(
|
||||
@@ -1037,7 +1068,7 @@ export async function POST(request: Request) {
|
||||
});
|
||||
} else if (body.action === "task_bind_customer_chat") {
|
||||
const taskId = String(body.taskId ?? "").trim();
|
||||
const forbidden = await taskOwnerGate(taskId);
|
||||
const forbidden = await taskOwnerGate(taskId, { strict: true });
|
||||
if (forbidden) return forbidden;
|
||||
const task = await db
|
||||
.prepare(
|
||||
|
||||
Reference in New Issue
Block a user