Compare commits
8 Commits
ec021d1a95
...
5b23410549
| Author | SHA1 | Date |
|---|---|---|
|
|
5b23410549 | |
|
|
1b58246897 | |
|
|
71c62579dc | |
|
|
34ffa8ca4d | |
|
|
754142b2ca | |
|
|
4edca8c7cc | |
|
|
eb1add4c9a | |
|
|
4584c227ff |
|
|
@ -6,4 +6,5 @@ NODEDC_PLATFORM_ENV=../../NODEDC/platform/infra/.env
|
||||||
PORT=5173
|
PORT=5173
|
||||||
LAUNCHER_BASE_URL=http://launcher.local.nodedc
|
LAUNCHER_BASE_URL=http://launcher.local.nodedc
|
||||||
TASK_BASE_URL=http://task.local.nodedc
|
TASK_BASE_URL=http://task.local.nodedc
|
||||||
|
NODEDC_BIM_VIEWER_PUBLIC_URL=http://localhost:8080
|
||||||
NODEDC_INTERNAL_ACCESS_TOKEN=change-me-generate-with-platform-init-dev-env
|
NODEDC_INTERNAL_ACCESS_TOKEN=change-me-generate-with-platform-init-dev-env
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ const clientTypes = new Set(["company", "person"]);
|
||||||
const clientStatuses = new Set(["active", "suspended", "demo", "expired"]);
|
const clientStatuses = new Set(["active", "suspended", "demo", "expired"]);
|
||||||
const userStatuses = new Set(["invited", "active", "blocked"]);
|
const userStatuses = new Set(["invited", "active", "blocked"]);
|
||||||
const membershipRoles = new Set(["client_owner", "client_admin", "member"]);
|
const membershipRoles = new Set(["client_owner", "client_admin", "member"]);
|
||||||
|
const coreAssistantRoles = new Set(["blocked", "member", "admin"]);
|
||||||
const grantTargetTypes = new Set(["client", "group", "user"]);
|
const grantTargetTypes = new Set(["client", "group", "user"]);
|
||||||
const appRoles = new Set(["viewer", "member", "admin", "owner"]);
|
const appRoles = new Set(["viewer", "member", "admin", "owner"]);
|
||||||
const grantStatuses = new Set(["active", "disabled"]);
|
const grantStatuses = new Set(["active", "disabled"]);
|
||||||
|
|
@ -62,6 +63,50 @@ const publicPoolClient = {
|
||||||
createdAt: "2026-05-09T00:00:00.000Z",
|
createdAt: "2026-05-09T00:00:00.000Z",
|
||||||
updatedAt: "2026-05-09T00:00:00.000Z",
|
updatedAt: "2026-05-09T00:00:00.000Z",
|
||||||
};
|
};
|
||||||
|
const bimViewerService = {
|
||||||
|
id: "service_bim_viewer",
|
||||||
|
slug: "bim-viewer",
|
||||||
|
title: "BIM VIEWER",
|
||||||
|
subtitle: "BIM и CAD просмотр",
|
||||||
|
description: "Персональный просмотр BIM/CAD моделей, версий и публичных ссылок.",
|
||||||
|
fullDescription:
|
||||||
|
"Standalone BIM/CAD viewer NODE.DC для рабочих моделей, вложений Operational Core, персонального модельного хранилища и безопасного внешнего просмотра по share-ссылкам.",
|
||||||
|
url: "https://bim.nodedc.tech",
|
||||||
|
launchUrl: "https://bim.nodedc.tech",
|
||||||
|
logoutUrl: "https://bim.nodedc.tech/logout",
|
||||||
|
loginSyncUrl: "https://bim.nodedc.tech/auth/login-sync",
|
||||||
|
accentColor: "#8FE3C8",
|
||||||
|
fallbackGradient: "linear-gradient(132deg, rgba(143, 227, 200, 0.78), rgba(24, 70, 64, 0.9) 46%, #080B0F 84%)",
|
||||||
|
status: "active",
|
||||||
|
order: 25,
|
||||||
|
authentikApplicationSlug: "bim-viewer",
|
||||||
|
authentikGroupName: "nodedc:bim:access",
|
||||||
|
isAvailableForAllNewClients: true,
|
||||||
|
createdAt: "2026-06-18T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-06-18T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
const bimViewerDefaultGrants = [
|
||||||
|
{
|
||||||
|
id: "grant_bim_viewer_client_romashka",
|
||||||
|
serviceId: "service_bim_viewer",
|
||||||
|
targetType: "client",
|
||||||
|
targetId: "client_romashka",
|
||||||
|
appRole: "member",
|
||||||
|
status: "active",
|
||||||
|
createdAt: "2026-06-18T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-06-18T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "grant_bim_viewer_client_public_pool",
|
||||||
|
serviceId: "service_bim_viewer",
|
||||||
|
targetType: "client",
|
||||||
|
targetId: publicPoolClientId,
|
||||||
|
appRole: "member",
|
||||||
|
status: "active",
|
||||||
|
createdAt: "2026-06-18T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-06-18T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
brand: {
|
brand: {
|
||||||
logoLinkUrl: "/",
|
logoLinkUrl: "/",
|
||||||
|
|
@ -478,6 +523,7 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
role: pickEnum(payload?.role, membershipRoles, "member"),
|
role: pickEnum(payload?.role, membershipRoles, "member"),
|
||||||
status: pickEnum(payload?.membershipStatus, new Set(["active", "disabled"]), "active"),
|
status: pickEnum(payload?.membershipStatus, new Set(["active", "disabled"]), "active"),
|
||||||
|
coreAssistantRole: pickEnum(payload?.coreAssistantRole, coreAssistantRoles, "member"),
|
||||||
invitedByUserId: actor.id,
|
invitedByUserId: actor.id,
|
||||||
inviteId: null,
|
inviteId: null,
|
||||||
source: "launcher",
|
source: "launcher",
|
||||||
|
|
@ -530,9 +576,11 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||||
|
|
||||||
membership.role = "client_owner";
|
membership.role = "client_owner";
|
||||||
membership.status = "active";
|
membership.status = "active";
|
||||||
|
membership.coreAssistantRole = "admin";
|
||||||
} else {
|
} else {
|
||||||
membership.role = pickEnum(payload?.role, membershipRoles, membership.role);
|
membership.role = pickEnum(payload?.role, membershipRoles, membership.role);
|
||||||
membership.status = pickEnum(payload?.status, new Set(["active", "disabled"]), membership.status);
|
membership.status = pickEnum(payload?.status, new Set(["active", "disabled"]), membership.status);
|
||||||
|
membership.coreAssistantRole = pickEnum(payload?.coreAssistantRole, coreAssistantRoles, membership.coreAssistantRole ?? "member");
|
||||||
}
|
}
|
||||||
membership.updatedAt = isoNow();
|
membership.updatedAt = isoNow();
|
||||||
|
|
||||||
|
|
@ -542,7 +590,7 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||||
objectName: user.email,
|
objectName: user.email,
|
||||||
clientId: membership.clientId,
|
clientId: membership.clientId,
|
||||||
result: "success",
|
result: "success",
|
||||||
details: `Role: ${membership.role}; status: ${membership.status}`,
|
details: `Role: ${membership.role}; status: ${membership.status}; coreAssistantRole: ${membership.coreAssistantRole}`,
|
||||||
});
|
});
|
||||||
markPendingSync(data, user, "user");
|
markPendingSync(data, user, "user");
|
||||||
|
|
||||||
|
|
@ -1553,6 +1601,7 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||||
if (membership) {
|
if (membership) {
|
||||||
membership.role = invite.role;
|
membership.role = invite.role;
|
||||||
membership.status = "active";
|
membership.status = "active";
|
||||||
|
membership.coreAssistantRole = normalizeCoreAssistantRole(membership.coreAssistantRole);
|
||||||
membership.invitedByUserId = invite.invitedByUserId ?? membership.invitedByUserId ?? null;
|
membership.invitedByUserId = invite.invitedByUserId ?? membership.invitedByUserId ?? null;
|
||||||
membership.inviteId = invite.id;
|
membership.inviteId = invite.id;
|
||||||
membership.source = invite.source ?? membership.source ?? "launcher";
|
membership.source = invite.source ?? membership.source ?? "launcher";
|
||||||
|
|
@ -1565,6 +1614,7 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
role: invite.role,
|
role: invite.role,
|
||||||
status: "active",
|
status: "active",
|
||||||
|
coreAssistantRole: "member",
|
||||||
invitedByUserId: invite.invitedByUserId ?? null,
|
invitedByUserId: invite.invitedByUserId ?? null,
|
||||||
inviteId: invite.id,
|
inviteId: invite.id,
|
||||||
source: invite.source ?? "launcher",
|
source: invite.source ?? "launcher",
|
||||||
|
|
@ -2274,6 +2324,7 @@ function normalizeData(payload) {
|
||||||
...client,
|
...client,
|
||||||
integrations: normalizeClientIntegrations(client.integrations),
|
integrations: normalizeClientIntegrations(client.integrations),
|
||||||
}));
|
}));
|
||||||
|
data.memberships = data.memberships.map(normalizeMembership).filter(Boolean);
|
||||||
data.services = data.services.map(normalizeService);
|
data.services = data.services.map(normalizeService);
|
||||||
data.accessRequests = data.accessRequests.map(normalizeAccessRequest).filter(Boolean);
|
data.accessRequests = data.accessRequests.map(normalizeAccessRequest).filter(Boolean);
|
||||||
data.revokedAccounts = data.revokedAccounts.map(normalizeRevokedAccount).filter(Boolean);
|
data.revokedAccounts = data.revokedAccounts.map(normalizeRevokedAccount).filter(Boolean);
|
||||||
|
|
@ -2281,9 +2332,90 @@ function normalizeData(payload) {
|
||||||
data.taskerInviteRequests = data.taskerInviteRequests.map(normalizeTaskerInviteRequest).filter(Boolean);
|
data.taskerInviteRequests = data.taskerInviteRequests.map(normalizeTaskerInviteRequest).filter(Boolean);
|
||||||
data.engineWorkflowAccessRequests = data.engineWorkflowAccessRequests.map(normalizeEngineWorkflowAccessRequest).filter(Boolean);
|
data.engineWorkflowAccessRequests = data.engineWorkflowAccessRequests.map(normalizeEngineWorkflowAccessRequest).filter(Boolean);
|
||||||
applyProtectedLauncherUserInvariants(data);
|
applyProtectedLauncherUserInvariants(data);
|
||||||
|
applyBimViewerServiceInvariants(data);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyBimViewerServiceInvariants(data) {
|
||||||
|
let service = data.services.find(
|
||||||
|
(candidate) => candidate.id === bimViewerService.id || candidate.slug === bimViewerService.slug
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
service = normalizeService({ ...bimViewerService });
|
||||||
|
data.services.push(service);
|
||||||
|
} else {
|
||||||
|
service.id = service.id || bimViewerService.id;
|
||||||
|
service.slug = service.slug || bimViewerService.slug;
|
||||||
|
service.title = service.title || bimViewerService.title;
|
||||||
|
service.subtitle = service.subtitle || bimViewerService.subtitle;
|
||||||
|
service.description = service.description || bimViewerService.description;
|
||||||
|
service.fullDescription = service.fullDescription || bimViewerService.fullDescription;
|
||||||
|
service.url = service.url || bimViewerService.url;
|
||||||
|
service.launchUrl = service.launchUrl || service.url || bimViewerService.launchUrl;
|
||||||
|
service.logoutUrl = service.logoutUrl || bimViewerService.logoutUrl;
|
||||||
|
service.accentColor = service.accentColor || bimViewerService.accentColor;
|
||||||
|
service.fallbackGradient = service.fallbackGradient || bimViewerService.fallbackGradient;
|
||||||
|
service.status = service.status || bimViewerService.status;
|
||||||
|
service.order = service.order ?? bimViewerService.order;
|
||||||
|
service.authentikApplicationSlug = service.authentikApplicationSlug || bimViewerService.authentikApplicationSlug;
|
||||||
|
service.authentikGroupName = service.authentikGroupName || bimViewerService.authentikGroupName;
|
||||||
|
service.isAvailableForAllNewClients = service.isAvailableForAllNewClients ?? true;
|
||||||
|
service.createdAt = service.createdAt || bimViewerService.createdAt;
|
||||||
|
service.updatedAt = service.updatedAt || bimViewerService.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceId = service.id || bimViewerService.id;
|
||||||
|
for (const defaultGrant of bimViewerDefaultGrants) {
|
||||||
|
const grant = { ...defaultGrant, serviceId };
|
||||||
|
const exists = data.grants.some(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.serviceId === grant.serviceId &&
|
||||||
|
candidate.targetType === grant.targetType &&
|
||||||
|
candidate.targetId === grant.targetId
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
data.grants.push({ ...grant });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureStaticSyncStatus(data, {
|
||||||
|
id: "sync_bim_viewer_authentik",
|
||||||
|
objectId: serviceId,
|
||||||
|
objectName: bimViewerService.title,
|
||||||
|
objectType: "service",
|
||||||
|
});
|
||||||
|
for (const defaultGrant of bimViewerDefaultGrants) {
|
||||||
|
const grant = { ...defaultGrant, serviceId };
|
||||||
|
ensureStaticSyncStatus(data, {
|
||||||
|
id: `sync_${grant.id}`,
|
||||||
|
objectId: grant.id,
|
||||||
|
objectName: `${bimViewerService.slug}:${grant.targetType}:${grant.targetId}`,
|
||||||
|
objectType: "grant",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureStaticSyncStatus(data, status) {
|
||||||
|
const exists = data.syncStatuses.some(
|
||||||
|
(candidate) => candidate.target === "authentik" && candidate.objectType === status.objectType && candidate.objectId === status.objectId
|
||||||
|
);
|
||||||
|
if (exists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.syncStatuses.push({
|
||||||
|
id: status.id,
|
||||||
|
objectId: status.objectId,
|
||||||
|
objectName: status.objectName,
|
||||||
|
objectType: status.objectType,
|
||||||
|
target: "authentik",
|
||||||
|
state: "pending",
|
||||||
|
lastSyncAt: null,
|
||||||
|
error: null,
|
||||||
|
updatedAt: "2026-06-18T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function applyProtectedLauncherUserInvariants(data) {
|
function applyProtectedLauncherUserInvariants(data) {
|
||||||
const rootUser = data.users.find((user) => isProtectedLauncherUserId(user.id));
|
const rootUser = data.users.find((user) => isProtectedLauncherUserId(user.id));
|
||||||
if (!rootUser) return;
|
if (!rootUser) return;
|
||||||
|
|
@ -2296,9 +2428,10 @@ function applyProtectedLauncherUserInvariants(data) {
|
||||||
|
|
||||||
for (const membership of data.memberships) {
|
for (const membership of data.memberships) {
|
||||||
if (membership.userId !== rootUser.id) continue;
|
if (membership.userId !== rootUser.id) continue;
|
||||||
if (membership.role !== "client_owner" || membership.status !== "active") {
|
if (membership.role !== "client_owner" || membership.status !== "active" || membership.coreAssistantRole !== "admin") {
|
||||||
membership.role = "client_owner";
|
membership.role = "client_owner";
|
||||||
membership.status = "active";
|
membership.status = "active";
|
||||||
|
membership.coreAssistantRole = "admin";
|
||||||
membership.updatedAt = now;
|
membership.updatedAt = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2358,6 +2491,21 @@ function upsertSystemProtectedUserGrant(data, user, service, appRole, now) {
|
||||||
|
|
||||||
function normalizeService(service) {
|
function normalizeService(service) {
|
||||||
if (typeof service !== "object" || service === null) return service;
|
if (typeof service !== "object" || service === null) return service;
|
||||||
|
if (service.id === "service_bim_viewer" || service.slug === "bim-viewer" || service.authentikApplicationSlug === "bim-viewer") {
|
||||||
|
const legacyLocalUrl = (value) => value === "http://localhost:8080" || value === "http://127.0.0.1:8080";
|
||||||
|
return {
|
||||||
|
...service,
|
||||||
|
url: !service.url || legacyLocalUrl(service.url) ? "https://bim.nodedc.tech" : service.url,
|
||||||
|
launchUrl:
|
||||||
|
!service.launchUrl || legacyLocalUrl(service.launchUrl)
|
||||||
|
? "https://bim.nodedc.tech"
|
||||||
|
: service.launchUrl,
|
||||||
|
logoutUrl: service.logoutUrl || "https://bim.nodedc.tech/logout",
|
||||||
|
loginSyncUrl: service.loginSyncUrl || "https://bim.nodedc.tech/auth/login-sync",
|
||||||
|
authentikApplicationSlug: service.authentikApplicationSlug || "bim-viewer",
|
||||||
|
authentikGroupName: service.authentikGroupName || "nodedc:bim:access",
|
||||||
|
};
|
||||||
|
}
|
||||||
if (service.id !== "service_nodedc" && service.slug !== "nodedc" && service.authentikApplicationSlug !== "nodedc") return service;
|
if (service.id !== "service_nodedc" && service.slug !== "nodedc" && service.authentikApplicationSlug !== "nodedc") return service;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -2380,6 +2528,26 @@ function normalizeService(service) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMembership(payload) {
|
||||||
|
if (typeof payload !== "object" || payload === null) return null;
|
||||||
|
const now = isoNow();
|
||||||
|
const userId = optionalString(payload.userId, "");
|
||||||
|
const fallbackAssistantRole = isProtectedLauncherUserId(userId) ? "admin" : "member";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
role: pickEnum(payload.role, membershipRoles, "member"),
|
||||||
|
status: pickEnum(payload.status, new Set(["active", "disabled"]), "active"),
|
||||||
|
coreAssistantRole: normalizeCoreAssistantRole(payload.coreAssistantRole, fallbackAssistantRole),
|
||||||
|
createdAt: optionalString(payload.createdAt, now),
|
||||||
|
updatedAt: optionalString(payload.updatedAt, now),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCoreAssistantRole(value, fallback = "member") {
|
||||||
|
return pickEnum(value, coreAssistantRoles, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeServiceModuleEntitlement(payload) {
|
function normalizeServiceModuleEntitlement(payload) {
|
||||||
if (typeof payload !== "object" || payload === null) return null;
|
if (typeof payload !== "object" || payload === null) return null;
|
||||||
const clientId = nullableString(payload.clientId);
|
const clientId = nullableString(payload.clientId);
|
||||||
|
|
@ -2995,6 +3163,7 @@ function ensureEngineWorkflowPublicPoolMembership(data, user, request, actor, no
|
||||||
membership.status = "active";
|
membership.status = "active";
|
||||||
if (!wasActive) {
|
if (!wasActive) {
|
||||||
membership.role = "member";
|
membership.role = "member";
|
||||||
|
membership.coreAssistantRole = normalizeCoreAssistantRole(membership.coreAssistantRole);
|
||||||
membership.source = "engine_workflow_access_request";
|
membership.source = "engine_workflow_access_request";
|
||||||
membership.sourceEngineWorkflowAccessRequestId = request.id;
|
membership.sourceEngineWorkflowAccessRequestId = request.id;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3013,6 +3182,7 @@ function ensureEngineWorkflowPublicPoolMembership(data, user, request, actor, no
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
role: "member",
|
role: "member",
|
||||||
status: "active",
|
status: "active",
|
||||||
|
coreAssistantRole: "member",
|
||||||
invitedByUserId,
|
invitedByUserId,
|
||||||
inviteId: null,
|
inviteId: null,
|
||||||
source: "engine_workflow_access_request",
|
source: "engine_workflow_access_request",
|
||||||
|
|
@ -3036,6 +3206,7 @@ function ensurePublicPoolMembershipActiveForServiceAccess(data, user, actor, now
|
||||||
|
|
||||||
membership.role = "member";
|
membership.role = "member";
|
||||||
membership.status = "active";
|
membership.status = "active";
|
||||||
|
membership.coreAssistantRole = normalizeCoreAssistantRole(membership.coreAssistantRole);
|
||||||
membership.invitedByUserId = membership.invitedByUserId ?? actor?.id ?? null;
|
membership.invitedByUserId = membership.invitedByUserId ?? actor?.id ?? null;
|
||||||
membership.source = membership.source ?? "launcher";
|
membership.source = membership.source ?? "launcher";
|
||||||
membership.updatedAt = now;
|
membership.updatedAt = now;
|
||||||
|
|
@ -3206,6 +3377,7 @@ function applyInviteRegistration(data, token, payload, { commit, provisioning =
|
||||||
if (membership) {
|
if (membership) {
|
||||||
membership.role = invite.role;
|
membership.role = invite.role;
|
||||||
membership.status = "active";
|
membership.status = "active";
|
||||||
|
membership.coreAssistantRole = normalizeCoreAssistantRole(membership.coreAssistantRole);
|
||||||
membership.invitedByUserId = invite.invitedByUserId ?? membership.invitedByUserId ?? null;
|
membership.invitedByUserId = invite.invitedByUserId ?? membership.invitedByUserId ?? null;
|
||||||
membership.inviteId = invite.id;
|
membership.inviteId = invite.id;
|
||||||
membership.source = invite.source ?? membership.source ?? "launcher";
|
membership.source = invite.source ?? membership.source ?? "launcher";
|
||||||
|
|
@ -3218,6 +3390,7 @@ function applyInviteRegistration(data, token, payload, { commit, provisioning =
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
role: invite.role,
|
role: invite.role,
|
||||||
status: "active",
|
status: "active",
|
||||||
|
coreAssistantRole: "member",
|
||||||
invitedByUserId: invite.invitedByUserId ?? null,
|
invitedByUserId: invite.invitedByUserId ?? null,
|
||||||
inviteId: invite.id,
|
inviteId: invite.id,
|
||||||
source: invite.source ?? "launcher",
|
source: invite.source ?? "launcher",
|
||||||
|
|
@ -3376,6 +3549,7 @@ function upsertAccessRequestMembership(data, user, requestPayload, options = {})
|
||||||
if (existingMembership) {
|
if (existingMembership) {
|
||||||
existingMembership.role = role;
|
existingMembership.role = role;
|
||||||
existingMembership.status = options.status ?? existingMembership.status;
|
existingMembership.status = options.status ?? existingMembership.status;
|
||||||
|
existingMembership.coreAssistantRole = normalizeCoreAssistantRole(existingMembership.coreAssistantRole);
|
||||||
existingMembership.invitedByUserId = options.invitedByUserId ?? existingMembership.invitedByUserId ?? null;
|
existingMembership.invitedByUserId = options.invitedByUserId ?? existingMembership.invitedByUserId ?? null;
|
||||||
existingMembership.source = existingMembership.source ?? "access_request";
|
existingMembership.source = existingMembership.source ?? "access_request";
|
||||||
existingMembership.updatedAt = now;
|
existingMembership.updatedAt = now;
|
||||||
|
|
@ -3388,6 +3562,7 @@ function upsertAccessRequestMembership(data, user, requestPayload, options = {})
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
role,
|
role,
|
||||||
status: options.status ?? "disabled",
|
status: options.status ?? "disabled",
|
||||||
|
coreAssistantRole: "member",
|
||||||
invitedByUserId: options.invitedByUserId ?? null,
|
invitedByUserId: options.invitedByUserId ?? null,
|
||||||
inviteId: null,
|
inviteId: null,
|
||||||
source: "access_request",
|
source: "access_request",
|
||||||
|
|
@ -3490,6 +3665,7 @@ function sanitizeServicePatch(payload, service) {
|
||||||
"url",
|
"url",
|
||||||
"launchUrl",
|
"launchUrl",
|
||||||
"logoutUrl",
|
"logoutUrl",
|
||||||
|
"loginSyncUrl",
|
||||||
"iconUrl",
|
"iconUrl",
|
||||||
"coverImageUrl",
|
"coverImageUrl",
|
||||||
"coverMediaKind",
|
"coverMediaKind",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -339,7 +339,7 @@ export function LauncherApp() {
|
||||||
if (isRedirecting) return;
|
if (isRedirecting) return;
|
||||||
|
|
||||||
isRedirecting = true;
|
isRedirecting = true;
|
||||||
redirectToLogin("/auth/login?prompt=login");
|
redirectToLogin("/auth/login");
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export interface Service {
|
||||||
url: string;
|
url: string;
|
||||||
launchUrl?: string | null;
|
launchUrl?: string | null;
|
||||||
logoutUrl?: string | null;
|
logoutUrl?: string | null;
|
||||||
|
loginSyncUrl?: string | null;
|
||||||
iconUrl?: string | null;
|
iconUrl?: string | null;
|
||||||
coverImageUrl?: string | null;
|
coverImageUrl?: string | null;
|
||||||
coverMediaKind?: MediaKind | null;
|
coverMediaKind?: MediaKind | null;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const source = readFileSync(resolve(process.cwd(), "server", "dev-server.mjs"), "utf8");
|
||||||
|
|
||||||
|
function sourceBlock(startMarker: string, endMarker: string) {
|
||||||
|
const start = source.indexOf(startMarker);
|
||||||
|
expect(start, `Missing source marker: ${startMarker}`).toBeGreaterThanOrEqual(0);
|
||||||
|
const end = source.indexOf(endMarker, start + startMarker.length);
|
||||||
|
expect(end, `Missing end marker after: ${startMarker}`).toBeGreaterThan(start);
|
||||||
|
return source.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant-ready Launcher admin routes", () => {
|
||||||
|
it("keeps control-plane read behind Launcher admin guard", () => {
|
||||||
|
expect(source).toContain('app.get("/api/admin/control-plane", requireLauncherAdmin');
|
||||||
|
expect(source).toContain("res.json(scopeAdminSnapshot(req));");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps assistant gateway auth narrow and route-allowlisted", () => {
|
||||||
|
expect(source).toContain('const assistantGatewayName = "ontology-core"');
|
||||||
|
expect(source).toContain("const assistantGatewayAdminRouteAllowlist = [");
|
||||||
|
expect(source).toContain('{ method: "GET", pattern: /^\\/api\\/admin\\/control-plane$/ }');
|
||||||
|
expect(source).toContain('{ method: "PATCH", pattern: /^\\/api\\/admin\\/users\\/[^/?#]+\\/profile$/ }');
|
||||||
|
expect(source).toContain('{ method: "PATCH", pattern: /^\\/api\\/admin\\/memberships\\/[^/?#]+$/ }');
|
||||||
|
expect(source).toContain("resolveAssistantGatewayAdminContext(req)");
|
||||||
|
expect(source).toContain("assistant_gateway_route_not_allowlisted");
|
||||||
|
expect(source).toContain('readRequestHeader(req, "x-nodedc-assistant-actor-user-id")');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes browser assistant actions through session-owned Launcher proxy context", () => {
|
||||||
|
const block = sourceBlock('app.post("/api/assistant/actions"', 'app.get("/api/events"');
|
||||||
|
const builderBlock = sourceBlock("function buildAssistantActionProxyCommand", "function getAiWorkspaceAssistantBaseUrl");
|
||||||
|
|
||||||
|
expect(block).toContain("requireSession");
|
||||||
|
expect(block).toContain("getLauncherProfileContext(req.nodedcSession)");
|
||||||
|
expect(block).toContain("getRuntimeSessionContext(req.nodedcSession)");
|
||||||
|
expect(block).toContain("buildAssistantActionProxyCommand");
|
||||||
|
expect(block).toContain("requestAiWorkspaceAssistantJson");
|
||||||
|
expect(builderBlock).toContain("stripClientAssistantContext");
|
||||||
|
expect(builderBlock).toContain('"actorUserId"');
|
||||||
|
expect(builderBlock).toContain('"assistantRole"');
|
||||||
|
expect(builderBlock).toContain('"groups"');
|
||||||
|
expect(builderBlock).toContain('"removesOwnAccess"');
|
||||||
|
expect(builderBlock).toContain('"hasAlternativeAdminPath"');
|
||||||
|
expect(builderBlock).toContain("resolveAssistantActionOwnerContext");
|
||||||
|
expect(builderBlock).toContain("assistantActionRemovesOwnAccess");
|
||||||
|
expect(builderBlock).toContain("hasAlternativeAssistantAdminPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps user profile mutation guarded, idempotent, audited, and scoped", () => {
|
||||||
|
const block = sourceBlock('app.patch("/api/admin/users/:userId/profile"', 'app.delete("/api/admin/users/:userId"');
|
||||||
|
|
||||||
|
expect(block).toContain("requireLauncherAdmin");
|
||||||
|
expect(block).toContain("assertAdminCanManageUser(req, res, req.params.userId)");
|
||||||
|
expect(block).toContain("sendAdminIdempotentJson(req, res");
|
||||||
|
expect(block).toContain("controlPlaneStore.updateUserProfile");
|
||||||
|
expect(block).toContain('publishControlPlaneEvent("admin.user.updated"');
|
||||||
|
expect(block).toContain("scopeAdminMutationResult");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps membership mutation guarded, idempotent, audited, and scoped", () => {
|
||||||
|
const block = sourceBlock('app.patch("/api/admin/memberships/:membershipId"', 'app.delete("/api/admin/memberships/:membershipId"');
|
||||||
|
|
||||||
|
expect(block).toContain("requireLauncherAdmin");
|
||||||
|
expect(block).toContain("assertAdminCanManageMembership(req, res, membership)");
|
||||||
|
expect(block).toContain("sendAdminIdempotentJson(req, res");
|
||||||
|
expect(block).toContain("controlPlaneStore.updateMembership");
|
||||||
|
expect(block).toContain('publishControlPlaneEvent("admin.membership.updated"');
|
||||||
|
expect(block).toContain("scopeAdminMutationResult");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps hard delete routes outside assistant-ready routes and root-guarded where available", () => {
|
||||||
|
const userDeleteBlock = sourceBlock('app.delete("/api/admin/users/:userId"', 'app.post("/api/admin/users/:userId/provision-authentik"');
|
||||||
|
|
||||||
|
expect(userDeleteBlock).toContain("requireRootLauncherAdmin");
|
||||||
|
expect(userDeleteBlock).toContain("controlPlaneStore.deleteUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps admin mutation idempotency conflict and replay behavior", () => {
|
||||||
|
expect(source).toContain("sendAdminIdempotentJson");
|
||||||
|
expect(source).toContain("idempotency_key_conflict");
|
||||||
|
expect(source).toContain('res.setHeader("Idempotency-Replayed", "true")');
|
||||||
|
expect(source).toContain("buildAdminMutationFingerprint");
|
||||||
|
expect(source).toContain("stableJson(req.body ?? null)");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -23,6 +23,7 @@ export interface LauncherUser {
|
||||||
|
|
||||||
export type ClientMembershipRole = "client_owner" | "client_admin" | "member";
|
export type ClientMembershipRole = "client_owner" | "client_admin" | "member";
|
||||||
export type ClientMembershipStatus = "active" | "disabled";
|
export type ClientMembershipStatus = "active" | "disabled";
|
||||||
|
export type CoreAssistantAccessRole = "blocked" | "member" | "admin";
|
||||||
|
|
||||||
export interface ClientMembership {
|
export interface ClientMembership {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -30,6 +31,7 @@ export interface ClientMembership {
|
||||||
userId: string;
|
userId: string;
|
||||||
role: ClientMembershipRole;
|
role: ClientMembershipRole;
|
||||||
status: ClientMembershipStatus;
|
status: ClientMembershipStatus;
|
||||||
|
coreAssistantRole?: CoreAssistantAccessRole;
|
||||||
invitedByUserId?: string | null;
|
invitedByUserId?: string | null;
|
||||||
inviteId?: string | null;
|
inviteId?: string | null;
|
||||||
source?: "launcher" | "access_request" | "tasker_workspace_invite" | "engine_workflow_access_request" | null;
|
source?: "launcher" | "access_request" | "tasker_workspace_invite" | "engine_workflow_access_request" | null;
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ export interface MeResponse {
|
||||||
clientName: string;
|
clientName: string;
|
||||||
role: ClientMembershipRole;
|
role: ClientMembershipRole;
|
||||||
status: ClientMembership["status"];
|
status: ClientMembership["status"];
|
||||||
|
coreAssistantRole: ClientMembership["coreAssistantRole"];
|
||||||
}>;
|
}>;
|
||||||
activeClientId: string;
|
activeClientId: string;
|
||||||
permissions: LauncherPermissions;
|
permissions: LauncherPermissions;
|
||||||
|
|
@ -214,7 +215,7 @@ export function normalizeLauncherData(data: Partial<LauncherData> | null | undef
|
||||||
return {
|
return {
|
||||||
clients: Array.isArray(payload.clients) ? payload.clients : mockClients,
|
clients: Array.isArray(payload.clients) ? payload.clients : mockClients,
|
||||||
users: Array.isArray(payload.users) ? payload.users : mockUsers,
|
users: Array.isArray(payload.users) ? payload.users : mockUsers,
|
||||||
memberships: Array.isArray(payload.memberships) ? payload.memberships : mockMemberships,
|
memberships: Array.isArray(payload.memberships) ? payload.memberships.map(normalizeMembership) : mockMemberships.map(normalizeMembership),
|
||||||
groups: Array.isArray(payload.groups) ? payload.groups : mockGroups,
|
groups: Array.isArray(payload.groups) ? payload.groups : mockGroups,
|
||||||
services: Array.isArray(payload.services) ? payload.services : mockServices,
|
services: Array.isArray(payload.services) ? payload.services : mockServices,
|
||||||
grants: Array.isArray(payload.grants) ? payload.grants : mockGrants,
|
grants: Array.isArray(payload.grants) ? payload.grants : mockGrants,
|
||||||
|
|
@ -259,6 +260,7 @@ export function buildMe(data: LauncherData, userId: string, requestedClientId?:
|
||||||
clientName: client.name,
|
clientName: client.name,
|
||||||
role: "client_owner" as const,
|
role: "client_owner" as const,
|
||||||
status: "active" as const,
|
status: "active" as const,
|
||||||
|
coreAssistantRole: "admin" as const,
|
||||||
}))
|
}))
|
||||||
: data.memberships
|
: data.memberships
|
||||||
.filter((membership) => membership.userId === user.id)
|
.filter((membership) => membership.userId === user.id)
|
||||||
|
|
@ -267,6 +269,7 @@ export function buildMe(data: LauncherData, userId: string, requestedClientId?:
|
||||||
clientName: getClient(data, membership.clientId).name,
|
clientName: getClient(data, membership.clientId).name,
|
||||||
role: membership.role,
|
role: membership.role,
|
||||||
status: membership.status,
|
status: membership.status,
|
||||||
|
coreAssistantRole: normalizeCoreAssistantRole(membership.coreAssistantRole),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const fallbackClientId =
|
const fallbackClientId =
|
||||||
|
|
@ -304,6 +307,21 @@ export function buildMe(data: LauncherData, userId: string, requestedClientId?:
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMembership(membership: ClientMembership): ClientMembership {
|
||||||
|
return {
|
||||||
|
...membership,
|
||||||
|
coreAssistantRole: normalizeCoreAssistantRole(membership.coreAssistantRole, membership.userId === "user_root" ? "admin" : "member"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCoreAssistantRole(
|
||||||
|
value: ClientMembership["coreAssistantRole"] | null | undefined,
|
||||||
|
fallback: NonNullable<ClientMembership["coreAssistantRole"]> = "member"
|
||||||
|
): NonNullable<ClientMembership["coreAssistantRole"]> {
|
||||||
|
if (value === "blocked" || value === "member" || value === "admin") return value;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildLauncherServices(data: LauncherData, userId: string, activeClientId: string): LauncherServiceView[] {
|
export function buildLauncherServices(data: LauncherData, userId: string, activeClientId: string): LauncherServiceView[] {
|
||||||
const me = buildMe(data, userId, activeClientId);
|
const me = buildMe(data, userId, activeClientId);
|
||||||
const user = getUser(data, userId);
|
const user = getUser(data, userId);
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,27 @@ export const mockServices: Service[] = [
|
||||||
createdAt: "2026-04-01T10:00:00Z",
|
createdAt: "2026-04-01T10:00:00Z",
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "service_bim_viewer",
|
||||||
|
slug: "bim-viewer",
|
||||||
|
title: "BIM VIEWER",
|
||||||
|
subtitle: "BIM и CAD просмотр",
|
||||||
|
description: "Персональный просмотр BIM/CAD моделей, версий и публичных ссылок.",
|
||||||
|
fullDescription:
|
||||||
|
"Standalone BIM/CAD viewer NODE.DC для рабочих моделей, вложений Operational Core, персонального модельного хранилища и безопасного внешнего просмотра по share-ссылкам.",
|
||||||
|
url: "https://bim.nodedc.tech",
|
||||||
|
launchUrl: "https://bim.nodedc.tech",
|
||||||
|
logoutUrl: "https://bim.nodedc.tech/logout",
|
||||||
|
accentColor: "#8FE3C8",
|
||||||
|
fallbackGradient: "linear-gradient(132deg, rgba(143, 227, 200, 0.78), rgba(24, 70, 64, 0.9) 46%, #080B0F 84%)",
|
||||||
|
status: "active",
|
||||||
|
order: 25,
|
||||||
|
authentikApplicationSlug: "bim-viewer",
|
||||||
|
authentikGroupName: "nodedc:bim:access",
|
||||||
|
isAvailableForAllNewClients: true,
|
||||||
|
createdAt: "2026-06-18T00:00:00.000Z",
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "service_1c",
|
id: "service_1c",
|
||||||
slug: "1c-assistant",
|
slug: "1c-assistant",
|
||||||
|
|
@ -211,6 +232,8 @@ export const mockGrants: ServiceGrant[] = [
|
||||||
grant("grant_dctouch_nodedc_admins", "service_nodedc", "group", "group_dctouch_admins", "admin"),
|
grant("grant_dctouch_nodedc_admins", "service_nodedc", "group", "group_dctouch_admins", "admin"),
|
||||||
grant("grant_engine_user_silver_psih_yahoo_com", "service_nodedc", "user", "user_silver_psih", "member"),
|
grant("grant_engine_user_silver_psih_yahoo_com", "service_nodedc", "user", "user_silver_psih", "member"),
|
||||||
grant("grant_engine_user_constr_dc_yahoo_com", "service_nodedc", "user", "user_constr_dc_yahoo_com", "viewer"),
|
grant("grant_engine_user_constr_dc_yahoo_com", "service_nodedc", "user", "user_constr_dc_yahoo_com", "viewer"),
|
||||||
|
grant("grant_bim_viewer_client_romashka", "service_bim_viewer", "client", "client_romashka", "member"),
|
||||||
|
grant("grant_bim_viewer_client_public_pool", "service_bim_viewer", "client", "client_public_pool", "member"),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mockExceptions: ServiceAccessException[] = [];
|
export const mockExceptions: ServiceAccessException[] = [];
|
||||||
|
|
@ -235,6 +258,23 @@ export const mockSyncStatuses: SyncStatus[] = [
|
||||||
),
|
),
|
||||||
sync("sync_dctouch_groups_authentik", "client_romashka:groups", "DCTOUCH groups", "group", "authentik", "pending"),
|
sync("sync_dctouch_groups_authentik", "client_romashka:groups", "DCTOUCH groups", "group", "authentik", "pending"),
|
||||||
sync("sync_task_manager_authentik", "service_task_manager", "OPERATIONAL CORE", "service", "authentik", "synced"),
|
sync("sync_task_manager_authentik", "service_task_manager", "OPERATIONAL CORE", "service", "authentik", "synced"),
|
||||||
|
sync("sync_bim_viewer_authentik", "service_bim_viewer", "BIM VIEWER", "service", "authentik", "pending"),
|
||||||
|
sync(
|
||||||
|
"sync_grant_bim_viewer_client_romashka",
|
||||||
|
"grant_bim_viewer_client_romashka",
|
||||||
|
"bim-viewer:client:client_romashka",
|
||||||
|
"grant",
|
||||||
|
"authentik",
|
||||||
|
"pending"
|
||||||
|
),
|
||||||
|
sync(
|
||||||
|
"sync_grant_bim_viewer_client_public_pool",
|
||||||
|
"grant_bim_viewer_client_public_pool",
|
||||||
|
"bim-viewer:client:client_public_pool",
|
||||||
|
"grant",
|
||||||
|
"authentik",
|
||||||
|
"pending"
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mockAuditEvents: AuditEvent[] = [
|
export const mockAuditEvents: AuditEvent[] = [
|
||||||
|
|
@ -258,12 +298,15 @@ function membership(
|
||||||
role: ClientMembership["role"],
|
role: ClientMembership["role"],
|
||||||
status: ClientMembership["status"] = "active"
|
status: ClientMembership["status"] = "active"
|
||||||
): ClientMembership {
|
): ClientMembership {
|
||||||
|
const coreAssistantRole: ClientMembership["coreAssistantRole"] = userId === "user_root" ? "admin" : "member";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
clientId,
|
clientId,
|
||||||
userId,
|
userId,
|
||||||
role,
|
role,
|
||||||
status,
|
status,
|
||||||
|
coreAssistantRole,
|
||||||
createdAt: "2026-04-01T10:00:00Z",
|
createdAt: "2026-04-01T10:00:00Z",
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3761,6 +3761,11 @@ code {
|
||||||
color: #ffd0d0;
|
color: #ffd0d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.access-cell--assistant-admin {
|
||||||
|
background: rgba(199, 166, 255, 0.16);
|
||||||
|
color: #e6d8ff;
|
||||||
|
}
|
||||||
|
|
||||||
.access-cell--active {
|
.access-cell--active {
|
||||||
background: rgba(255, 255, 255, 0.12);
|
background: rgba(255, 255, 255, 0.12);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ import type {
|
||||||
ClientMembership,
|
ClientMembership,
|
||||||
ClientMembershipRole,
|
ClientMembershipRole,
|
||||||
ClientMembershipStatus,
|
ClientMembershipStatus,
|
||||||
|
CoreAssistantAccessRole,
|
||||||
LauncherUser,
|
LauncherUser,
|
||||||
LauncherUserStatus,
|
LauncherUserStatus,
|
||||||
} from "../../entities/user/types";
|
} from "../../entities/user/types";
|
||||||
|
|
@ -1414,6 +1415,12 @@ const membershipRoleOptions: Array<NodeDcSelectOption<ClientMembershipRole>> = [
|
||||||
{ value: "member", label: "Member", description: "Пользователь" },
|
{ value: "member", label: "Member", description: "Пользователь" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const coreAssistantAccessRoleOptions: Array<NodeDcSelectOption<CoreAssistantAccessRole>> = [
|
||||||
|
{ value: "blocked", label: "Нет доступа", description: "Ассистент скрыт и недоступен", tone: "red" },
|
||||||
|
{ value: "member", label: "Есть доступ", description: "Личный ассистент в разрешённых контурах", tone: "green" },
|
||||||
|
{ value: "admin", label: "Админ", description: "Admin-команды в рамках Launcher scope", tone: "violet" },
|
||||||
|
];
|
||||||
|
|
||||||
const inviteRoleOptions: Array<NodeDcSelectOption<ClientMembershipRole>> = [
|
const inviteRoleOptions: Array<NodeDcSelectOption<ClientMembershipRole>> = [
|
||||||
{ value: "member", label: "Member" },
|
{ value: "member", label: "Member" },
|
||||||
{ value: "client_admin", label: "Client Admin" },
|
{ value: "client_admin", label: "Client Admin" },
|
||||||
|
|
@ -1559,6 +1566,14 @@ function membershipRoleLabel(role: ClientMembershipRole): string {
|
||||||
return membershipRoleOptions.find((option) => option.value === role)?.label ?? role;
|
return membershipRoleOptions.find((option) => option.value === role)?.label ?? role;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveCoreAssistantRole(membership: ClientMembership): CoreAssistantAccessRole {
|
||||||
|
return membership.coreAssistantRole ?? "member";
|
||||||
|
}
|
||||||
|
|
||||||
|
function coreAssistantAccessRoleLabel(role: CoreAssistantAccessRole): string {
|
||||||
|
return coreAssistantAccessRoleOptions.find((option) => option.value === role)?.label ?? role;
|
||||||
|
}
|
||||||
|
|
||||||
function inviteSourceLabel(invite: Invite): string {
|
function inviteSourceLabel(invite: Invite): string {
|
||||||
if (invite.source === "access_request") return "Заявка доступа";
|
if (invite.source === "access_request") return "Заявка доступа";
|
||||||
if (invite.source === "tasker_workspace_invite") return "Workspace-инвайт";
|
if (invite.source === "tasker_workspace_invite") return "Workspace-инвайт";
|
||||||
|
|
@ -3149,7 +3164,7 @@ function AccessSection({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessGridTemplateColumns = `15rem repeat(2, 9.7rem) repeat(${matrix.services.length}, 11.25rem)`;
|
const accessGridTemplateColumns = `15rem repeat(2, 9.7rem) 12.6rem repeat(${matrix.services.length}, 11.25rem)`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("access-layout", isPublicPoolContext && "access-layout--single")}>
|
<div className={cn("access-layout", isPublicPoolContext && "access-layout--single")}>
|
||||||
|
|
@ -3169,6 +3184,9 @@ function AccessSection({
|
||||||
<div className="access-grid-head" role="columnheader">
|
<div className="access-grid-head" role="columnheader">
|
||||||
MAIN ROLE
|
MAIN ROLE
|
||||||
</div>
|
</div>
|
||||||
|
<div className="access-grid-head" role="columnheader">
|
||||||
|
NDC Core Assistant
|
||||||
|
</div>
|
||||||
{matrix.services.map((service) => (
|
{matrix.services.map((service) => (
|
||||||
<div key={service.id} className="access-grid-head" role="columnheader">
|
<div key={service.id} className="access-grid-head" role="columnheader">
|
||||||
{service.title}
|
{service.title}
|
||||||
|
|
@ -3211,6 +3229,13 @@ function AccessSection({
|
||||||
onChange={(role) => onUpdateMembership(membership.id, { role })}
|
onChange={(role) => onUpdateMembership(membership.id, { role })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="access-grid-cell" role="cell">
|
||||||
|
<CoreAssistantAccessControl
|
||||||
|
value={resolveCoreAssistantRole(membership)}
|
||||||
|
protectedUser={protectedUser || protectedFromActor}
|
||||||
|
onChange={(coreAssistantRole) => onUpdateMembership(membership.id, { coreAssistantRole })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{matrix.services.map((service) => {
|
{matrix.services.map((service) => {
|
||||||
const cell = matrix.cells.find((item) => item.userId === user.id && item.serviceId === service.id)!;
|
const cell = matrix.cells.find((item) => item.userId === user.id && item.serviceId === service.id)!;
|
||||||
const active = selectedCell?.userId === user.id && selectedCell.serviceId === service.id;
|
const active = selectedCell?.userId === user.id && selectedCell.serviceId === service.id;
|
||||||
|
|
@ -3540,6 +3565,50 @@ function MainRoleControl({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CoreAssistantAccessControl({
|
||||||
|
value,
|
||||||
|
protectedUser,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: CoreAssistantAccessRole;
|
||||||
|
protectedUser: boolean;
|
||||||
|
onChange: (value: CoreAssistantAccessRole) => void;
|
||||||
|
}) {
|
||||||
|
const label = coreAssistantAccessRoleLabel(value);
|
||||||
|
const toneClass =
|
||||||
|
value === "blocked"
|
||||||
|
? "access-cell--exception"
|
||||||
|
: value === "admin"
|
||||||
|
? "access-cell--assistant-admin"
|
||||||
|
: "access-cell--allowed";
|
||||||
|
|
||||||
|
if (protectedUser) {
|
||||||
|
return (
|
||||||
|
<span className={cn("access-cell access-cell--main access-cell--readonly", toneClass)}>
|
||||||
|
<strong>{label}</strong>
|
||||||
|
<span>Assistant</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeDcSelect
|
||||||
|
value={value}
|
||||||
|
options={coreAssistantAccessRoleOptions}
|
||||||
|
label="NDC Core Assistant"
|
||||||
|
minMenuWidth={244}
|
||||||
|
menuClassName="access-cell-menu"
|
||||||
|
onChange={onChange}
|
||||||
|
trigger={({ open, toggle, setTriggerRef, selectedOption }) => (
|
||||||
|
<button ref={setTriggerRef} className={cn("access-cell access-cell--main", toneClass)} type="button" aria-expanded={open} onClick={toggle}>
|
||||||
|
<strong>{selectedOption.label}</strong>
|
||||||
|
<span>Assistant</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function OperationalCoreAccessModal({
|
function OperationalCoreAccessModal({
|
||||||
data,
|
data,
|
||||||
me,
|
me,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue