ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: PROJECT-LEVEL ДОСТУПЫ OPERATIONAL CORE
This commit is contained in:
@@ -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
@@ -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)
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user