From 4584c227ff0538944078136d870771237c9552ab Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Thu, 18 Jun 2026 21:43:04 +0300 Subject: [PATCH] Add BIM viewer service launch integration --- server/control-plane-store.mjs | 122 +++++++++++++++++++++++++++++++++ server/dev-server.mjs | 33 ++++++++- src/shared/api/mockData.ts | 39 +++++++++++ 3 files changed, 193 insertions(+), 1 deletion(-) diff --git a/server/control-plane-store.mjs b/server/control-plane-store.mjs index 3893d52..092ff84 100644 --- a/server/control-plane-store.mjs +++ b/server/control-plane-store.mjs @@ -62,6 +62,48 @@ const publicPoolClient = { createdAt: "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.ru", + launchUrl: "https://bim.nodedc.ru", + 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 = { brand: { logoLinkUrl: "/", @@ -2281,9 +2323,89 @@ function normalizeData(payload) { data.taskerInviteRequests = data.taskerInviteRequests.map(normalizeTaskerInviteRequest).filter(Boolean); data.engineWorkflowAccessRequests = data.engineWorkflowAccessRequests.map(normalizeEngineWorkflowAccessRequest).filter(Boolean); applyProtectedLauncherUserInvariants(data); + applyBimViewerServiceInvariants(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.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) { const rootUser = data.users.find((user) => isProtectedLauncherUserId(user.id)); if (!rootUser) return; diff --git a/server/dev-server.mjs b/server/dev-server.mjs index 6ec0db2..e6ec41d 100644 --- a/server/dev-server.mjs +++ b/server/dev-server.mjs @@ -422,6 +422,21 @@ app.get("/api/services/:serviceSlug/launch", requireSession, (req, res) => { return; } + if (isBimViewerServiceSlug(serviceSlug)) { + const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user); + const targetUrl = new URL("/auth/nodedc/handoff", getBimViewerBaseUrl(app)); + const nextPath = sanitizeReturnTo(req.query.next_path || req.query.returnTo || "/"); + + targetUrl.searchParams.set("token", handoffToken); + + if (nextPath && nextPath !== "/") { + targetUrl.searchParams.set("next_path", nextPath); + } + + res.redirect(targetUrl.toString()); + return; + } + if (serviceSlug !== "task-manager") { res.redirect(app.openUrl || app.url || "/"); return; @@ -3031,6 +3046,11 @@ function isEngineServiceSlug(serviceSlug) { return slug === "nodedc" || slug === "engine" || slug === "nodedc-engine"; } +function isBimViewerServiceSlug(serviceSlug) { + const slug = sanitizeServiceSlug(serviceSlug); + return slug === "bim-viewer" || slug === "beam-viewer" || slug === "bim"; +} + function specialRequiredGroups(serviceOrSlug) { const slug = typeof serviceOrSlug === "string" ? serviceOrSlug : serviceOrSlug?.slug; const applicationSlug = typeof serviceOrSlug === "string" ? null : serviceOrSlug?.authentikApplicationSlug; @@ -3046,7 +3066,7 @@ function specialRequiredGroups(serviceOrSlug) { } function getServiceUrl(service) { - if (service.slug === "task-manager" || isEngineServiceSlug(service.slug)) { + if (service.slug === "task-manager" || isEngineServiceSlug(service.slug) || isBimViewerServiceSlug(service.slug)) { return `/api/services/${encodeURIComponent(service.slug)}/launch`; } @@ -3066,6 +3086,17 @@ function getTaskBaseUrl() { return taskBaseUrl.replace(/\/$/, ""); } +function getBimViewerBaseUrl(app = null) { + return String( + process.env.NODEDC_BIM_VIEWER_PUBLIC_URL || + process.env.BIM_VIEWER_BASE_URL || + app?.launchTargetUrl || + app?.openUrl || + app?.url || + "http://localhost:8080" + ).replace(/\/$/, ""); +} + function getEnginePublicBaseUrl() { return String( process.env.NODEDC_ENGINE_PUBLIC_URL || diff --git a/src/shared/api/mockData.ts b/src/shared/api/mockData.ts index 58322f5..701e3a8 100644 --- a/src/shared/api/mockData.ts +++ b/src/shared/api/mockData.ts @@ -113,6 +113,26 @@ export const mockServices: Service[] = [ createdAt: "2026-04-01T10:00:00Z", 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.ru", + launchUrl: "https://bim.nodedc.ru", + 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", slug: "1c-assistant", @@ -211,6 +231,8 @@ export const mockGrants: ServiceGrant[] = [ 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_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[] = []; @@ -235,6 +257,23 @@ export const mockSyncStatuses: SyncStatus[] = [ ), 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_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[] = [