ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: Tasker workspace adapter в Launcher
This commit is contained in:
@@ -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) =>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user