feat: harden tenant access and production operations
This commit is contained in:
+21
-4
@@ -141,6 +141,7 @@ import { createSsoTicket, handleOidcCallback, redeemSsoTicket, startOidcLogin }
|
||||
import { handleSamlCallback, isSamlProvider, samlServiceProviderMetadata, startSamlLogin } from "./saml.mjs";
|
||||
import { listWorkItems } from "./work-items.mjs";
|
||||
import { addTaskLink, createProjectTask, createTaskComment, getProjectTask, listProjectActivity, listProjectTasks, listTaskComments, removeTaskLink, updateProjectTask } from "./tasks.mjs";
|
||||
import { searchPlatform } from "./search.mjs";
|
||||
import { consumeRateLimit, rateLimitHeaders, rateLimitIdentity } from "./rate-limit.mjs";
|
||||
import { createDeliveryAccessLink, listDeliveryAccessFeedback, listDeliveryAccessLinks, readPublicDeliveryFile, resolvePublicDeliveryPortal, revokeDeliveryAccessLink, submitPublicDeliveryFeedback } from "./delivery-portal.mjs";
|
||||
|
||||
@@ -953,12 +954,17 @@ function createInvitation(context, organizationId, body) {
|
||||
throw httpError(409, "invitation_already_pending", "该邮箱已有待处理邀请,请重新发送或先撤销旧邀请", { email });
|
||||
}
|
||||
assertOrganizationSeatAvailable(organizationId, { email });
|
||||
const roleKey = body.roleKey || "writer";
|
||||
if (!dbGet("SELECT key FROM roles WHERE key = ?", [roleKey])) throw httpError(400, "role_invalid", "邀请角色不存在", { roleKey });
|
||||
const workspaceId = body.workspaceId || null;
|
||||
if (workspaceId && !dbGet("SELECT id FROM workspaces WHERE id = ? AND organization_id = ?", [workspaceId, organizationId])) throw httpError(400, "workspace_invalid", "邀请目标工作区不属于当前组织");
|
||||
const projectId = body.projectId || null;
|
||||
if (projectId && !workspaceId) throw httpError(400, "workspace_required_for_project_invitation", "项目级邀请必须同时指定所属工作区");
|
||||
if (projectId && !dbGet("SELECT id FROM projects WHERE id = ? AND workspace_id = ?", [projectId, workspaceId])) throw httpError(400, "project_invalid", "邀请目标项目不属于当前工作区");
|
||||
const invitationScope = projectId ? "project" : workspaceId ? "workspace" : "organization";
|
||||
const defaultRole = invitationScope === "project" ? "project_editor" : invitationScope === "workspace" ? "writer" : "org_member";
|
||||
const requestedRoleKey = String(body.roleKey || defaultRole).trim();
|
||||
const role = roleForScope(requestedRoleKey, invitationScope);
|
||||
if (invitationScope === "organization" && role.key === "org_owner") throw httpError(400, "owner_invitation_not_allowed", "组织所有者不能通过普通邀请授予,请在成员管理中完成所有者转移");
|
||||
const roleKey = role.key;
|
||||
const id = createId("invite");
|
||||
const inviteToken = `invite-${randomBytes(24).toString("base64url")}`;
|
||||
const tokenHash = createHash("sha256").update(inviteToken).digest("hex");
|
||||
@@ -1019,12 +1025,14 @@ function updateWorkspaceMember(context, workspaceId, userId, body) {
|
||||
const member = dbGet("SELECT * FROM workspace_members WHERE workspace_id = ? AND user_id = ?", [workspaceId, userId]);
|
||||
if (!member) throw httpError(404, "member_not_found", "工作区成员不存在");
|
||||
const roleKey = String(body.roleKey || member.role_key);
|
||||
const accessMode = String(body.accessMode || member.access_mode || "all");
|
||||
const status = String(body.status || member.status);
|
||||
roleForScope(roleKey, "workspace");
|
||||
if (!["all", "project-only"].includes(accessMode)) throw httpError(400, "workspace_access_mode_invalid", "工作区访问范围只能是 all 或 project-only");
|
||||
if (!["active", "suspended"].includes(status)) throw httpError(400, "member_status_invalid", "成员状态无效");
|
||||
const timestamp = new Date().toISOString();
|
||||
dbRun("UPDATE workspace_members SET role_key = ?, status = ?, updated_at = ? WHERE workspace_id = ? AND user_id = ?", [roleKey, status, timestamp, workspaceId, userId]);
|
||||
addAudit({ context, action: "workspace.member.updated", targetType: "workspace_member", targetId: `${workspaceId}:${userId}`, metadata: { previousRole: member.role_key, roleKey, previousStatus: member.status, status } });
|
||||
dbRun("UPDATE workspace_members SET role_key = ?, access_mode = ?, status = ?, updated_at = ? WHERE workspace_id = ? AND user_id = ?", [roleKey, accessMode, status, timestamp, workspaceId, userId]);
|
||||
addAudit({ context, action: "workspace.member.updated", targetType: "workspace_member", targetId: `${workspaceId}:${userId}`, metadata: { previousRole: member.role_key, roleKey, previousAccessMode: member.access_mode || "all", accessMode, previousStatus: member.status, status } });
|
||||
return { members: workspaceMembers(workspaceId) };
|
||||
}
|
||||
|
||||
@@ -1508,6 +1516,15 @@ createServer(async (req, res) => {
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/context") return send(res, 200, platformPayload(resolveContext(req.headers, url.searchParams)));
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/search") {
|
||||
const context = resolveContext(req.headers, url.searchParams);
|
||||
return send(res, 200, searchPlatform(context, {
|
||||
query: url.searchParams.get("q") || url.searchParams.get("query") || "",
|
||||
scope: url.searchParams.get("scope") || "workspace",
|
||||
limit: url.searchParams.get("limit") || 40
|
||||
}));
|
||||
}
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/work-items") {
|
||||
const context = resolveContext(req.headers, url.searchParams);
|
||||
return send(res, 200, listWorkItems(context, { limit: url.searchParams.get("limit") }));
|
||||
|
||||
Reference in New Issue
Block a user