ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: PROJECT-LEVEL ДОСТУПЫ OPERATIONAL CORE

This commit is contained in:
DCCONSTRUCTIONS
2026-05-08 15:19:38 +03:00
parent 652a6ef0c5
commit 11dd8d1043
8 changed files with 782 additions and 34 deletions
+109
View File
@@ -15,6 +15,7 @@ const collectionKeys = [
"syncStatuses",
"auditEvents",
"taskManagerMemberships",
"taskManagerProjectMemberships",
];
const clientTypes = new Set(["company", "person"]);
@@ -201,6 +202,9 @@ export function createControlPlaneStore({ projectRoot }) {
data.taskManagerMemberships = data.taskManagerMemberships.filter(
(membership) => !(membership.clientId === client.id && membership.userId === user.id && membership.workspaceSlug === workspaceSlug)
);
data.taskManagerProjectMemberships = data.taskManagerProjectMemberships.filter(
(membership) => !(membership.clientId === client.id && membership.userId === user.id && membership.workspaceSlug === workspaceSlug)
);
addAuditEvent(data, actor, {
action: "Снят Tasker workspace",
@@ -215,6 +219,73 @@ export function createControlPlaneStore({ projectRoot }) {
return { data };
}
async function recordTaskManagerProjectMembership(payload, identity) {
const data = readData();
const actor = resolveActor(data, identity);
const client = findById(data.clients, payload?.clientId, "client");
const user = findById(data.users, payload?.userId, "user");
const taskManager = typeof payload?.taskManager === "object" && payload.taskManager !== null ? payload.taskManager : {};
const membership = typeof taskManager.membership === "object" && taskManager.membership !== null ? taskManager.membership : {};
const workspace = typeof membership.workspace === "object" && membership.workspace !== null ? membership.workspace : {};
const project = typeof membership.project === "object" && membership.project !== null ? membership.project : {};
upsertTaskManagerProjectMembership(data, {
clientId: client.id,
userId: user.id,
workspaceSlug: workspace.slug ?? payload?.workspaceSlug,
workspaceName: workspace.name ?? payload?.workspaceName ?? null,
projectId: project.id ?? payload?.projectId,
projectIdentifier: project.identifier ?? payload?.projectIdentifier ?? null,
projectName: project.name ?? payload?.projectName ?? null,
role: normalizeTaskManagerMembershipRole(payload?.role),
planeUserId: membership.member?.id ?? null,
planeRole: typeof membership.role === "number" ? membership.role : null,
});
addAuditEvent(data, actor, {
action: "Назначен Tasker project",
objectType: "task-manager-project-membership",
objectName: user.name,
clientId: client.id,
result: "success",
details: `Workspace: ${workspace.name ?? workspace.slug ?? payload?.workspaceSlug}; Project: ${project.name ?? project.identifier ?? payload?.projectId}; Role: ${payload?.role ?? "member"}`,
});
await writeData(data);
return { data };
}
async function removeTaskManagerProjectMembership(payload, identity) {
const data = readData();
const actor = resolveActor(data, identity);
const client = findById(data.clients, payload?.clientId, "client");
const user = findById(data.users, payload?.userId, "user");
const workspaceSlug = requireString(payload?.workspaceSlug ?? client.integrations?.taskManager?.workspaceSlug, "workspaceSlug");
const projectId = requireString(payload?.projectId, "projectId");
data.taskManagerProjectMemberships = data.taskManagerProjectMemberships.filter(
(membership) =>
!(
membership.clientId === client.id &&
membership.userId === user.id &&
membership.workspaceSlug === workspaceSlug &&
membership.projectId === projectId
)
);
addAuditEvent(data, actor, {
action: "Снят Tasker project",
objectType: "task-manager-project-membership",
objectName: user.name,
clientId: client.id,
result: "success",
details: `Workspace: ${workspaceSlug}; Project: ${projectId}`,
});
await writeData(data);
return { data };
}
async function deleteClient(clientId, identity) {
const data = readData();
const actor = resolveActor(data, identity);
@@ -1089,7 +1160,9 @@ export function createControlPlaneStore({ projectRoot }) {
reorderServices,
retrySync,
markUserAuthentikProvisioned,
recordTaskManagerProjectMembership,
recordTaskManagerWorkspaceMembership,
removeTaskManagerProjectMembership,
removeTaskManagerWorkspaceMembership,
setUserServiceAccess,
updateClient,
@@ -1233,6 +1306,42 @@ function upsertTaskManagerMembership(data, payload) {
return nextMembership;
}
function upsertTaskManagerProjectMembership(data, payload) {
const workspaceSlug = requireString(payload.workspaceSlug, "workspaceSlug");
const projectId = requireString(payload.projectId, "projectId");
const existingMembership = data.taskManagerProjectMemberships.find(
(membership) =>
membership.clientId === payload.clientId &&
membership.userId === payload.userId &&
membership.workspaceSlug === workspaceSlug &&
membership.projectId === projectId
);
const nextMembership = {
id:
existingMembership?.id ??
uniqueId(data.taskManagerProjectMemberships, "tasker_project_mem", `${payload.clientId}-${payload.userId}-${workspaceSlug}-${projectId}`),
clientId: payload.clientId,
userId: payload.userId,
workspaceSlug,
workspaceName: nullableStringWithFallback(payload.workspaceName, existingMembership?.workspaceName ?? null),
projectId,
projectIdentifier: nullableStringWithFallback(payload.projectIdentifier, existingMembership?.projectIdentifier ?? null),
projectName: nullableStringWithFallback(payload.projectName, existingMembership?.projectName ?? null),
role: normalizeTaskManagerMembershipRole(payload.role),
planeUserId: nullableStringWithFallback(payload.planeUserId, existingMembership?.planeUserId ?? null),
planeRole: typeof payload.planeRole === "number" ? payload.planeRole : (existingMembership?.planeRole ?? null),
updatedAt: isoNow(),
};
if (existingMembership) {
Object.assign(existingMembership, nextMembership);
return existingMembership;
}
data.taskManagerProjectMemberships.push(nextMembership);
return nextMembership;
}
function resolveActor(data, identity) {
const user = data.users.find(
(item) =>
+149 -4
View File
@@ -521,13 +521,29 @@ app.get("/api/admin/clients", requireLauncherAdmin, (req, res) => {
});
app.get("/api/admin/task-manager/workspaces", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!req.nodedcAdminScope?.isRoot) {
res.json({ ok: true, workspaces: [] });
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspaces/");
if (req.nodedcAdminScope?.isRoot) {
res.json(taskManager);
return;
}
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspaces/");
res.json(taskManager);
const allowedWorkspaceSlugs = new Set(
req.nodedcAdminScope.snapshot.data.clients
.filter((client) => req.nodedcAdminScope.clientIds.has(client.id))
.flatMap((client) => {
const workspaces = Array.isArray(client.integrations?.taskManager?.workspaces)
? client.integrations.taskManager.workspaces
: [];
const slugs = workspaces.map((workspace) => workspace?.slug).filter((slug) => typeof slug === "string" && slug.trim());
const legacySlug = client.integrations?.taskManager?.workspaceSlug;
return legacySlug ? [...slugs, legacySlug] : slugs;
})
);
res.json({
...taskManager,
workspaces: (taskManager.workspaces ?? []).filter((workspace) => allowedWorkspaceSlugs.has(workspace.slug)),
});
}));
app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncherAdmin, asyncRoute(async (req, res) => {
@@ -667,6 +683,132 @@ app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncher
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
}));
app.post("/api/admin/task-manager/project-memberships/ensure", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const clientId = typeof req.body?.clientId === "string" ? req.body.clientId : "";
const userId = typeof req.body?.userId === "string" ? req.body.userId : "";
const client = snapshot.data.clients.find((candidate) => candidate.id === clientId);
const user = snapshot.data.users.find((candidate) => candidate.id === userId);
if (!client) {
res.status(404).json({ ok: false, error: "client_not_found" });
return;
}
if (!user) {
res.status(404).json({ ok: false, error: "user_not_found" });
return;
}
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
return;
}
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug);
const projectId = normalizeOptionalText(req.body?.projectId);
const role = normalizeTaskManagerRole(req.body?.role);
if (!workspaceSlug) {
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
return;
}
if (!projectId) {
res.status(400).json({ ok: false, error: "task_manager_project_not_configured" });
return;
}
if (!role) {
res.status(400).json({ ok: false, error: "task_manager_role_invalid" });
return;
}
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/project-memberships/ensure/", {
method: "POST",
body: {
workspaceSlug,
projectId,
email: user.email,
subject: user.authentikUserId ?? undefined,
avatarUrl: resolveUserAvatarPublicUrl(user),
role,
setLastWorkspace: false,
},
});
const result = await controlPlaneStore.recordTaskManagerProjectMembership(
{
clientId: client.id,
userId: user.id,
workspaceSlug,
projectId,
role,
taskManager,
},
req.nodedcSession.user
);
publishControlPlaneEvent("admin.task-manager.project-membership.updated", [user.id]);
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
}));
app.post("/api/admin/task-manager/project-memberships/remove", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const clientId = typeof req.body?.clientId === "string" ? req.body.clientId : "";
const userId = typeof req.body?.userId === "string" ? req.body.userId : "";
const client = snapshot.data.clients.find((candidate) => candidate.id === clientId);
const user = snapshot.data.users.find((candidate) => candidate.id === userId);
if (!client) {
res.status(404).json({ ok: false, error: "client_not_found" });
return;
}
if (!user) {
res.status(404).json({ ok: false, error: "user_not_found" });
return;
}
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
return;
}
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug);
const projectId = normalizeOptionalText(req.body?.projectId);
if (!workspaceSlug) {
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
return;
}
if (!projectId) {
res.status(400).json({ ok: false, error: "task_manager_project_not_configured" });
return;
}
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/project-memberships/remove/", {
method: "POST",
body: {
workspaceSlug,
projectId,
email: user.email,
subject: user.authentikUserId ?? undefined,
},
});
const result = await controlPlaneStore.removeTaskManagerProjectMembership(
{
clientId: client.id,
userId: user.id,
workspaceSlug,
projectId,
},
req.nodedcSession.user
);
publishControlPlaneEvent("admin.task-manager.project-membership.updated", [user.id]);
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
}));
app.post("/api/admin/clients", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.createClient(req.body, req.nodedcSession.user);
res.status(201).json(result);
@@ -2209,6 +2351,9 @@ function scopeControlPlaneData(data, scope) {
taskManagerMemberships: data.taskManagerMemberships.filter(
(membership) => clientIds.has(membership.clientId) && userIds.has(membership.userId)
),
taskManagerProjectMemberships: data.taskManagerProjectMemberships.filter(
(membership) => clientIds.has(membership.clientId) && userIds.has(membership.userId)
),
syncStatuses: data.syncStatuses.filter(
(syncStatus) => clientIds.has(syncStatus.objectId) || userIds.has(syncStatus.objectId) || groupIds.has(syncStatus.objectId)
),