ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: Tasker workspace adapter в Launcher

This commit is contained in:
DCCONSTRUCTIONS
2026-05-06 10:36:20 +03:00
parent 897c7145f0
commit d4eba0ff3a
7 changed files with 474 additions and 63 deletions
+46
View File
@@ -103,6 +103,7 @@ export function createControlPlaneStore({ projectRoot }) {
demoEndsAt: nullableString(payload?.demoEndsAt),
contactName: nullableString(payload?.contactName),
contactEmail: nullableString(payload?.contactEmail),
integrations: normalizeClientIntegrations(payload?.integrations),
notes: nullableString(payload?.notes),
createdAt: now,
updatedAt: now,
@@ -138,6 +139,9 @@ export function createControlPlaneStore({ projectRoot }) {
client.demoEndsAt = nullableStringWithFallback(payload?.demoEndsAt, client.demoEndsAt ?? null);
client.contactName = nullableStringWithFallback(payload?.contactName, client.contactName ?? null);
client.contactEmail = nullableStringWithFallback(payload?.contactEmail, client.contactEmail ?? null);
if ("integrations" in (payload ?? {})) {
client.integrations = normalizeClientIntegrations(payload.integrations, client.integrations);
}
client.notes = nullableStringWithFallback(payload?.notes, client.notes ?? null);
client.updatedAt = isoNow();
@@ -154,6 +158,28 @@ export function createControlPlaneStore({ projectRoot }) {
return { client, data };
}
async function recordTaskManagerWorkspaceMembership(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 : {};
addAuditEvent(data, actor, {
action: "Назначен Tasker workspace",
objectType: "task-manager-membership",
objectName: user.name,
clientId: client.id,
result: "success",
details: `Workspace: ${workspace.name ?? workspace.slug ?? payload?.workspaceSlug}; Role: ${membership.role ?? payload?.role ?? "member"}`,
});
await writeData(data);
return { data };
}
async function deleteClient(clientId, identity) {
const data = readData();
const actor = resolveActor(data, identity);
@@ -1028,6 +1054,7 @@ export function createControlPlaneStore({ projectRoot }) {
reorderServices,
retrySync,
markUserAuthentikProvisioned,
recordTaskManagerWorkspaceMembership,
setUserServiceAccess,
updateClient,
updateGroup,
@@ -1052,6 +1079,10 @@ function normalizeData(payload) {
}
data.settings = normalizeSettings(data.settings);
data.clients = data.clients.map((client) => ({
...client,
integrations: normalizeClientIntegrations(client.integrations),
}));
return data;
}
@@ -1074,6 +1105,21 @@ function normalizeSettings(payload) {
};
}
function normalizeClientIntegrations(payload, fallback = {}) {
const integrations = typeof payload === "object" && payload !== null ? payload : {};
const fallbackIntegrations = typeof fallback === "object" && fallback !== null ? fallback : {};
const taskManager = typeof integrations.taskManager === "object" && integrations.taskManager !== null ? integrations.taskManager : {};
const fallbackTaskManager =
typeof fallbackIntegrations.taskManager === "object" && fallbackIntegrations.taskManager !== null ? fallbackIntegrations.taskManager : {};
return {
taskManager: {
workspaceSlug: nullableStringWithFallback(taskManager.workspaceSlug, fallbackTaskManager.workspaceSlug ?? null),
workspaceName: nullableStringWithFallback(taskManager.workspaceName, fallbackTaskManager.workspaceName ?? null),
},
};
}
function resolveActor(data, identity) {
const user = data.users.find(
(item) =>
+106
View File
@@ -520,6 +520,64 @@ app.get("/api/admin/clients", requireLauncherAdmin, (req, res) => {
res.json({ clients: snapshot.data.clients });
});
app.get("/api/admin/task-manager/workspaces", requireLauncherAdmin, asyncRoute(async (_req, res) => {
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspaces/");
res.json(taskManager);
}));
app.post("/api/admin/task-manager/workspace-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;
}
const membership = snapshot.data.memberships.find((candidate) => candidate.clientId === client.id && candidate.userId === user.id);
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug) ?? client.integrations?.taskManager?.workspaceSlug ?? null;
if (!workspaceSlug) {
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
return;
}
const role = normalizeTaskManagerRole(req.body?.role) ?? resolveTaskManagerRoleForMembership(membership?.role);
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspace-memberships/ensure/", {
method: "POST",
body: {
workspaceSlug,
email: user.email,
subject: user.authentikUserId ?? undefined,
role,
companyRole: membership?.role ?? null,
setLastWorkspace: req.body?.setLastWorkspace !== false,
},
});
const result = await controlPlaneStore.recordTaskManagerWorkspaceMembership(
{
clientId: client.id,
userId: user.id,
workspaceSlug,
role,
taskManager,
},
req.nodedcSession.user
);
publishControlPlaneEvent("admin.task-manager.workspace-membership.updated", [user.id]);
res.json({ ...result, taskManager });
}));
app.post("/api/admin/clients", requireLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.createClient(req.body, req.nodedcSession.user);
res.status(201).json(result);
@@ -1190,6 +1248,54 @@ function getTaskBaseUrl() {
return taskBaseUrl.replace(/\/$/, "");
}
async function requestTaskManagerInternalJson(pathname, init = {}) {
if (!config.internalAccessToken) {
throw new Error("NODE.DC internal access token is not configured");
}
const targetUrl = new URL(pathname, `${getTaskBaseUrl()}/`);
const hasBody = typeof init.body === "object" && init.body !== null;
const response = await fetch(targetUrl, {
method: init.method ?? (hasBody ? "POST" : "GET"),
headers: {
Accept: "application/json",
Authorization: `Bearer ${config.internalAccessToken}`,
...(hasBody ? { "Content-Type": "application/json" } : {}),
...(init.headers ?? {}),
},
body: hasBody ? JSON.stringify(init.body) : undefined,
});
const text = await response.text();
const payload = text ? parseJsonResponse(text, targetUrl.toString()) : {};
if (!response.ok) {
const error = typeof payload?.error === "string" ? payload.error : `Task Manager internal API failed: ${response.status}`;
throw new Error(error);
}
return payload;
}
function parseJsonResponse(text, url) {
try {
return JSON.parse(text);
} catch {
throw new Error(`Task Manager internal API returned non-JSON response: ${url}`);
}
}
function normalizeOptionalText(value) {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function normalizeTaskManagerRole(value) {
return value === "admin" || value === "member" ? value : null;
}
function resolveTaskManagerRoleForMembership(role) {
return role === "client_owner" || role === "client_admin" ? "admin" : "member";
}
function createServiceHandoff(serviceSlug, user) {
pruneExpiredServiceHandoffs();