Integrate BIM viewer with NodeDC auth and sharing
This commit is contained in:
parent
ef5b0a42a6
commit
4638acc04b
|
|
@ -4,3 +4,5 @@ node_modules
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
server/data/uploads/
|
server/data/uploads/
|
||||||
|
server/data/shares/
|
||||||
|
server/data/models/
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,15 @@ services:
|
||||||
image: node:22-alpine
|
image: node:22-alpine
|
||||||
working_dir: /beam/server
|
working_dir: /beam/server
|
||||||
command: node index.js
|
command: node index.js
|
||||||
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
|
NODEDC_BIM_AUTH_REQUIRED: "${NODEDC_BIM_AUTH_REQUIRED:-0}"
|
||||||
|
NODEDC_BIM_PUBLIC_URL: "${NODEDC_BIM_PUBLIC_URL:-http://localhost:8080}"
|
||||||
|
NODEDC_LAUNCHER_BASE_URL: "${NODEDC_LAUNCHER_BASE_URL:-http://localhost:5173}"
|
||||||
|
NODEDC_LAUNCHER_INTERNAL_URL: "${NODEDC_LAUNCHER_INTERNAL_URL:-http://host.docker.internal:5173}"
|
||||||
|
NODEDC_INTERNAL_ACCESS_TOKEN: "${NODEDC_INTERNAL_ACCESS_TOKEN:-}"
|
||||||
|
NODEDC_BIM_ALLOWED_ORIGINS: "${NODEDC_BIM_ALLOWED_ORIGINS:-http://localhost:5173,http://localhost:8090}"
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -16,8 +23,13 @@ services:
|
||||||
image: nodedc/bim-converter:local
|
image: nodedc/bim-converter:local
|
||||||
container_name: NodeDcBimConverter
|
container_name: NodeDcBimConverter
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
NODEDC_BIM_CONVERTER_UPLOADS_DIR: /beam/uploads
|
NODEDC_BIM_CONVERTER_UPLOADS_DIR: /beam/uploads
|
||||||
NODEDC_BIM_CONVERTER_INTERVAL_SECONDS: "10"
|
NODEDC_BIM_CONVERTER_INTERVAL_SECONDS: "10"
|
||||||
|
NODEDC_BIM_CONVERTER_MAX_ATTEMPTS: "3"
|
||||||
|
NODEDC_BIM_CONVERTER_TIMEOUT_SECONDS: "300"
|
||||||
|
NODEDC_BIM_CONVERTER_XCAF_TIMEOUT_SECONDS: "1200"
|
||||||
|
NODEDC_BIM_CONVERTER_CADQUERY_TIMEOUT_SECONDS: "1800"
|
||||||
volumes:
|
volumes:
|
||||||
- ./server/data/uploads:/beam/uploads
|
- ./server/data/uploads:/beam/uploads
|
||||||
|
|
|
||||||
|
|
@ -1203,19 +1203,11 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-project-modal .modal-close {
|
.save-project-modal .modal-close {
|
||||||
width: 32px;
|
flex: 0 0 auto;
|
||||||
height: 32px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid var(--modal-border);
|
|
||||||
background: var(--modal-focus);
|
|
||||||
color: var(--text);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-project-modal .modal-close:hover {
|
.save-project-modal .modal-close:hover {
|
||||||
background: var(--modal-border);
|
background: var(--env-glass-control-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-project-modal .input-label {
|
.save-project-modal .input-label {
|
||||||
|
|
@ -1239,6 +1231,11 @@ header {
|
||||||
box-shadow: 0 0 0 2px rgba(255, 234, 0, 0.08);
|
box-shadow: 0 0 0 2px rgba(255, 234, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save-project-modal input[readonly] {
|
||||||
|
cursor: text;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.save-project-modal .input-hint {
|
.save-project-modal .input-hint {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|
@ -1889,9 +1886,21 @@ header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: var(--tb-icon-pad, 10px);
|
padding: var(--tb-icon-pad, 10px);
|
||||||
|
line-height: 1;
|
||||||
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
|
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tb-btn svg {
|
||||||
|
display: block;
|
||||||
|
width: var(--tb-svg-size, 24px);
|
||||||
|
height: var(--tb-svg-size, 24px);
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.8;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
.tb-btn:hover {
|
.tb-btn:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
border-color: var(--toolbar-outline, rgba(76, 224, 210, 0.7));
|
border-color: var(--toolbar-outline, rgba(76, 224, 210, 0.7));
|
||||||
|
|
@ -1902,6 +1911,11 @@ header {
|
||||||
background: var(--toolbar-bg-active);
|
background: var(--toolbar-bg-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[data-viewer-mode="guest"] .bottom-toolbar,
|
||||||
|
body[data-viewer-mode="guest"] .inspector-restore-tray {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.templates-panel {
|
.templates-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: var(--templates-left, 72px);
|
left: var(--templates-left, 72px);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ const bottomToolbar = document.getElementById("bottomToolbar");
|
||||||
const measureButton = document.querySelector('[data-action="measure"]');
|
const measureButton = document.querySelector('[data-action="measure"]');
|
||||||
const settingsButton = document.querySelector('[data-action="settings"]');
|
const settingsButton = document.querySelector('[data-action="settings"]');
|
||||||
const templatesButton = document.querySelector('[data-action="templates"]');
|
const templatesButton = document.querySelector('[data-action="templates"]');
|
||||||
|
const shareButton = document.querySelector('[data-action="share"]');
|
||||||
const projectionToggle = document.querySelector('[data-action="projection"]');
|
const projectionToggle = document.querySelector('[data-action="projection"]');
|
||||||
const treeContainer = document.getElementById("treeContainer");
|
const treeContainer = document.getElementById("treeContainer");
|
||||||
const displayModeControl = document.getElementById("displayModeControl");
|
const displayModeControl = document.getElementById("displayModeControl");
|
||||||
|
|
@ -70,6 +71,13 @@ const deleteProjectClose = document.getElementById("deleteProjectClose");
|
||||||
const deleteProjectCancel = document.getElementById("deleteProjectCancel");
|
const deleteProjectCancel = document.getElementById("deleteProjectCancel");
|
||||||
const deleteProjectConfirm = document.getElementById("deleteProjectConfirm");
|
const deleteProjectConfirm = document.getElementById("deleteProjectConfirm");
|
||||||
const deleteProjectError = document.getElementById("deleteProjectError");
|
const deleteProjectError = document.getElementById("deleteProjectError");
|
||||||
|
const shareModalBackdrop = document.getElementById("shareModalBackdrop");
|
||||||
|
const shareModal = document.getElementById("shareModal");
|
||||||
|
const shareModalClose = document.getElementById("shareModalClose");
|
||||||
|
const shareModalCancel = document.getElementById("shareModalCancel");
|
||||||
|
const shareLinkInput = document.getElementById("shareLinkInput");
|
||||||
|
const shareCopyButton = document.getElementById("shareCopyButton");
|
||||||
|
const shareError = document.getElementById("shareError");
|
||||||
const projectNameSection = document.getElementById("projectNameSection");
|
const projectNameSection = document.getElementById("projectNameSection");
|
||||||
const projectNameInput = document.getElementById("projectNameInput");
|
const projectNameInput = document.getElementById("projectNameInput");
|
||||||
const projectNameError = document.getElementById("projectNameError");
|
const projectNameError = document.getElementById("projectNameError");
|
||||||
|
|
@ -110,6 +118,8 @@ const applyPanelOffsets = () => {
|
||||||
|
|
||||||
const API_BASE = "/api/projects";
|
const API_BASE = "/api/projects";
|
||||||
const MODEL_SETTINGS_API = "/api/model-settings";
|
const MODEL_SETTINGS_API = "/api/model-settings";
|
||||||
|
const AUTH_SESSION_API = "/api/auth/session";
|
||||||
|
const SHARES_API = "/api/shares";
|
||||||
|
|
||||||
const acceptByType = {
|
const acceptByType = {
|
||||||
xkt: ".xkt",
|
xkt: ".xkt",
|
||||||
|
|
@ -212,6 +222,9 @@ let treeView = null;
|
||||||
let currentBlobUrl = null;
|
let currentBlobUrl = null;
|
||||||
let modelCounter = 0;
|
let modelCounter = 0;
|
||||||
const activeModels = [];
|
const activeModels = [];
|
||||||
|
let viewerMode = "standard";
|
||||||
|
let runtimeSessionInfo = { authRequired: false, authenticated: false, canShare: true, user: null };
|
||||||
|
let startupSharePayload = null;
|
||||||
const memoryStore = new Map(); // url -> ArrayBuffer
|
const memoryStore = new Map(); // url -> ArrayBuffer
|
||||||
const blobUrls = [];
|
const blobUrls = [];
|
||||||
let gizmo = null;
|
let gizmo = null;
|
||||||
|
|
@ -421,6 +434,138 @@ const getPrimaryModelEntry = () => (
|
||||||
|
|
||||||
const hasModelSettingsTarget = () => !!getModelSettingsSrc(getPrimaryModelEntry());
|
const hasModelSettingsTarget = () => !!getModelSettingsSrc(getPrimaryModelEntry());
|
||||||
|
|
||||||
|
const isGuestMode = () => viewerMode === "guest";
|
||||||
|
|
||||||
|
const getShareTokenFromPath = () => {
|
||||||
|
const match = window.location.pathname.match(/^\/share\/([^/?#]+)/);
|
||||||
|
return match ? decodeURIComponent(match[1]) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyRuntimeMode = () => {
|
||||||
|
document.body.dataset.viewerMode = viewerMode;
|
||||||
|
if (bottomToolbar) {
|
||||||
|
bottomToolbar.classList.toggle("hidden", isGuestMode());
|
||||||
|
}
|
||||||
|
if (shareButton) {
|
||||||
|
const canShowShare = !isGuestMode() && runtimeSessionInfo.canShare && activeModels.length > 0;
|
||||||
|
shareButton.classList.toggle("hidden", !canShowShare);
|
||||||
|
shareButton.disabled = !canShowShare;
|
||||||
|
}
|
||||||
|
if (dropZone) {
|
||||||
|
dropZone.style.pointerEvents = isGuestMode() ? "none" : "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeRuntimeMode = async () => {
|
||||||
|
const shareToken = getShareTokenFromPath();
|
||||||
|
if (shareToken) {
|
||||||
|
viewerMode = "guest";
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${SHARES_API}/${encodeURIComponent(shareToken)}`, {
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Accept": "application/json" },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Share link failed: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
startupSharePayload = await response.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showError("Ссылка на модель недоступна");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(AUTH_SESSION_API, {
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Accept": "application/json" },
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
runtimeSessionInfo = await response.json();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("BIM session status unavailable", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyRuntimeMode();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShareableModelPayload = () => {
|
||||||
|
const entry = getPrimaryModelEntry();
|
||||||
|
if (!entry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const src = normalizeModelSettingsSrc(entry.meta?.artifactSrc || entry.src || entry.meta?.src);
|
||||||
|
const settingsSrc = getModelSettingsSrc(entry);
|
||||||
|
if (!src) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
assetId: entry.meta?.assetId || entry.meta?.conversion?.assetId || null,
|
||||||
|
name: entry.name || entry.meta?.label || src.split("/").pop() || "model",
|
||||||
|
settingsSrc: settingsSrc || src,
|
||||||
|
src,
|
||||||
|
type: entry.type || normalizeType(null, src),
|
||||||
|
versionId: entry.meta?.versionId || entry.meta?.conversion?.versionId || null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeShareModal = () => {
|
||||||
|
shareModal?.classList.add("hidden");
|
||||||
|
shareModalBackdrop?.classList.add("hidden");
|
||||||
|
if (shareError) shareError.textContent = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const openShareModal = async () => {
|
||||||
|
if (isGuestMode()) return;
|
||||||
|
const payload = getShareableModelPayload();
|
||||||
|
if (!payload) {
|
||||||
|
showError("Ссылку можно создать только для модели из BIM-хранилища");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shareModal?.classList.remove("hidden");
|
||||||
|
shareModalBackdrop?.classList.remove("hidden");
|
||||||
|
if (shareLinkInput) shareLinkInput.value = "Генерируем ссылку...";
|
||||||
|
if (shareCopyButton) shareCopyButton.disabled = true;
|
||||||
|
if (shareError) shareError.textContent = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(SHARES_API, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
const data = await response.json().catch(() => null);
|
||||||
|
if (!response.ok || !data?.url) {
|
||||||
|
throw new Error(data?.error || `Share create failed: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
if (shareLinkInput) {
|
||||||
|
shareLinkInput.value = data.url;
|
||||||
|
shareLinkInput.select();
|
||||||
|
}
|
||||||
|
if (shareCopyButton) shareCopyButton.disabled = false;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (shareLinkInput) shareLinkInput.value = "";
|
||||||
|
if (shareError) shareError.textContent = err?.message || "Не удалось создать ссылку";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyShareLink = async () => {
|
||||||
|
const value = shareLinkInput?.value || "";
|
||||||
|
if (!value) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard?.writeText(value);
|
||||||
|
} catch (_) {
|
||||||
|
shareLinkInput?.select();
|
||||||
|
document.execCommand?.("copy");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getDisplayModePreset = (mode) => {
|
const getDisplayModePreset = (mode) => {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "clay":
|
case "clay":
|
||||||
|
|
@ -1091,27 +1236,28 @@ const updatePanelsVisibility = () => {
|
||||||
if (treeContainer) treeContainer.classList.toggle("hidden", !hasModels);
|
if (treeContainer) treeContainer.classList.toggle("hidden", !hasModels);
|
||||||
const showActions = hasModels || !!currentProjectMeta || !!lastAttemptedProjectId;
|
const showActions = hasModels || !!currentProjectMeta || !!lastAttemptedProjectId;
|
||||||
if (saveProjectSection) {
|
if (saveProjectSection) {
|
||||||
saveProjectSection.classList.toggle("hidden", !showActions);
|
saveProjectSection.classList.toggle("hidden", isGuestMode() || !showActions);
|
||||||
}
|
}
|
||||||
if (projectNameSection) {
|
if (projectNameSection) {
|
||||||
projectNameSection.classList.toggle("hidden", !(currentProjectMeta || lastAttemptedProjectId));
|
projectNameSection.classList.toggle("hidden", isGuestMode() || !(currentProjectMeta || lastAttemptedProjectId));
|
||||||
}
|
}
|
||||||
if (saveProjectButton) {
|
if (saveProjectButton) {
|
||||||
saveProjectButton.disabled = !hasModels;
|
saveProjectButton.disabled = isGuestMode() || !hasModels;
|
||||||
}
|
}
|
||||||
if (saveModelSettingsButton) {
|
if (saveModelSettingsButton) {
|
||||||
saveModelSettingsButton.disabled = !hasModels || !hasModelSettingsTarget();
|
saveModelSettingsButton.disabled = isGuestMode() || !hasModels || !hasModelSettingsTarget();
|
||||||
}
|
}
|
||||||
if (deleteProjectButton) {
|
if (deleteProjectButton) {
|
||||||
deleteProjectButton.disabled = !(currentProjectMeta || lastAttemptedProjectId);
|
deleteProjectButton.disabled = isGuestMode() || !(currentProjectMeta || lastAttemptedProjectId);
|
||||||
}
|
}
|
||||||
if (projectNameInput) {
|
if (projectNameInput) {
|
||||||
const hasProject = !!(currentProjectMeta || lastAttemptedProjectId);
|
const hasProject = !isGuestMode() && !!(currentProjectMeta || lastAttemptedProjectId);
|
||||||
projectNameInput.disabled = !hasProject;
|
projectNameInput.disabled = !hasProject;
|
||||||
if (!hasProject) {
|
if (!hasProject) {
|
||||||
projectNameInput.value = "";
|
projectNameInput.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applyRuntimeMode();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setInspectorCollapsed = (collapsed) => {
|
const setInspectorCollapsed = (collapsed) => {
|
||||||
|
|
@ -2289,6 +2435,7 @@ const findProjectMetaById = (id) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitProjectRename = async () => {
|
const submitProjectRename = async () => {
|
||||||
|
if (isGuestMode()) return;
|
||||||
if (!projectNameInput || projectNameInput.disabled) return;
|
if (!projectNameInput || projectNameInput.disabled) return;
|
||||||
const newName = (projectNameInput.value || "").trim();
|
const newName = (projectNameInput.value || "").trim();
|
||||||
const targetId = currentProjectMeta?.id || lastAttemptedProjectId;
|
const targetId = currentProjectMeta?.id || lastAttemptedProjectId;
|
||||||
|
|
@ -2581,6 +2728,10 @@ const fetchModelSettings = async (src) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveCurrentModelSettings = async () => {
|
const saveCurrentModelSettings = async () => {
|
||||||
|
if (isGuestMode()) {
|
||||||
|
setStatus("Гостевой просмотр не сохраняет изменения");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const entry = getPrimaryModelEntry();
|
const entry = getPrimaryModelEntry();
|
||||||
const settingsSrc = getModelSettingsSrc(entry);
|
const settingsSrc = getModelSettingsSrc(entry);
|
||||||
if (!entry || !settingsSrc) {
|
if (!entry || !settingsSrc) {
|
||||||
|
|
@ -2697,6 +2848,7 @@ const initViewer = async () => {
|
||||||
preloadHidden.remove();
|
preloadHidden.remove();
|
||||||
}
|
}
|
||||||
document.body.style.opacity = "1";
|
document.body.style.opacity = "1";
|
||||||
|
await initializeRuntimeMode();
|
||||||
|
|
||||||
const sdk = await import(SDK_URL);
|
const sdk = await import(SDK_URL);
|
||||||
sdkRef = sdk;
|
sdkRef = sdk;
|
||||||
|
|
@ -3239,22 +3391,25 @@ const initViewer = async () => {
|
||||||
|
|
||||||
const loadStartupModelFromQuery = async () => {
|
const loadStartupModelFromQuery = async () => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const src = params.get("url") || params.get("src");
|
const sharedViewer = startupSharePayload?.viewer || null;
|
||||||
|
const src = sharedViewer?.src || params.get("url") || params.get("src");
|
||||||
reportViewerEvent("startup-query", {
|
reportViewerEvent("startup-query", {
|
||||||
search: window.location.search,
|
search: window.location.search,
|
||||||
|
mode: viewerMode,
|
||||||
|
hasShare: !!sharedViewer,
|
||||||
hasUrl: params.has("url"),
|
hasUrl: params.has("url"),
|
||||||
hasSrc: params.has("src"),
|
hasSrc: params.has("src"),
|
||||||
src,
|
src,
|
||||||
typeParam: params.get("type"),
|
typeParam: sharedViewer?.type || params.get("type"),
|
||||||
nameParam: params.get("name"),
|
nameParam: sharedViewer?.name || params.get("name"),
|
||||||
});
|
});
|
||||||
if (!src) {
|
if (!src) {
|
||||||
reportViewerEvent("startup-no-src", { search: window.location.search }, "error");
|
reportViewerEvent("startup-no-src", { search: window.location.search }, "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = params.get("name") || src.split("/").pop() || "model";
|
const name = sharedViewer?.name || params.get("name") || src.split("/").pop() || "model";
|
||||||
const type = normalizeType(params.get("type"), src) || normalizeType(guessTypeFromName(name));
|
const type = normalizeType(sharedViewer?.type || params.get("type"), src) || normalizeType(guessTypeFromName(name));
|
||||||
if (!type) {
|
if (!type) {
|
||||||
reportViewerEvent("startup-type-error", { src, name, typeParam: params.get("type") }, "error");
|
reportViewerEvent("startup-type-error", { src, name, typeParam: params.get("type") }, "error");
|
||||||
showError("Не удалось определить формат модели из URL");
|
showError("Не удалось определить формат модели из URL");
|
||||||
|
|
@ -3265,7 +3420,7 @@ const initViewer = async () => {
|
||||||
setStatus(`Подготовка: ${name}...`);
|
setStatus(`Подготовка: ${name}...`);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
let settingsSrc = params.get("settingsSrc") || params.get("sourceSrc") || params.get("originalSrc") || src;
|
let settingsSrc = sharedViewer?.settingsSrc || params.get("settingsSrc") || params.get("sourceSrc") || params.get("originalSrc") || src;
|
||||||
const applyQueryViewerOverrides = () => {
|
const applyQueryViewerOverrides = () => {
|
||||||
if (params.has("displayMode")) {
|
if (params.has("displayMode")) {
|
||||||
activeDisplayMode = params.get("displayMode") || "source";
|
activeDisplayMode = params.get("displayMode") || "source";
|
||||||
|
|
@ -3287,9 +3442,12 @@ const initViewer = async () => {
|
||||||
|
|
||||||
const startupMeta = {
|
const startupMeta = {
|
||||||
label: name,
|
label: name,
|
||||||
source: "query",
|
source: sharedViewer ? "share" : "query",
|
||||||
settingsSrc: normalizeModelSettingsSrc(settingsSrc) || normalizeModelSettingsSrc(src),
|
settingsSrc: normalizeModelSettingsSrc(settingsSrc) || normalizeModelSettingsSrc(src),
|
||||||
artifactSrc: normalizeModelSettingsSrc(src),
|
artifactSrc: normalizeModelSettingsSrc(src),
|
||||||
|
assetId: sharedViewer?.assetId || null,
|
||||||
|
versionId: sharedViewer?.versionId || null,
|
||||||
|
shareToken: startupSharePayload?.share?.token || getShareTokenFromPath(),
|
||||||
viewerSettings: null,
|
viewerSettings: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -3463,6 +3621,7 @@ const loadProjectSnapshot = async (project) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const openSaveModal = () => {
|
const openSaveModal = () => {
|
||||||
|
if (isGuestMode()) return;
|
||||||
if (!activeModels.length && !currentProjectMeta) return;
|
if (!activeModels.length && !currentProjectMeta) return;
|
||||||
const defaultName = currentProjectMeta?.name || "Новый проект";
|
const defaultName = currentProjectMeta?.name || "Новый проект";
|
||||||
if (saveProjectNameInput) {
|
if (saveProjectNameInput) {
|
||||||
|
|
@ -3492,6 +3651,7 @@ const loadProjectSnapshot = async (project) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDeleteModal = () => {
|
const openDeleteModal = () => {
|
||||||
|
if (isGuestMode()) return;
|
||||||
if (!currentProjectMeta && lastAttemptedProjectId) {
|
if (!currentProjectMeta && lastAttemptedProjectId) {
|
||||||
const meta = findProjectMetaById(lastAttemptedProjectId) || { id: lastAttemptedProjectId, name: "", type: "project" };
|
const meta = findProjectMetaById(lastAttemptedProjectId) || { id: lastAttemptedProjectId, name: "", type: "project" };
|
||||||
currentProjectMeta = meta;
|
currentProjectMeta = meta;
|
||||||
|
|
@ -3503,6 +3663,7 @@ const loadProjectSnapshot = async (project) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitDeleteProject = async () => {
|
const submitDeleteProject = async () => {
|
||||||
|
if (isGuestMode()) return;
|
||||||
const targetId = currentProjectMeta?.id || lastAttemptedProjectId;
|
const targetId = currentProjectMeta?.id || lastAttemptedProjectId;
|
||||||
if (!targetId) return;
|
if (!targetId) return;
|
||||||
if (deleteProjectError) deleteProjectError.textContent = "";
|
if (deleteProjectError) deleteProjectError.textContent = "";
|
||||||
|
|
@ -3537,6 +3698,7 @@ const loadProjectSnapshot = async (project) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitSaveProject = async (mode = "update") => {
|
const submitSaveProject = async (mode = "update") => {
|
||||||
|
if (isGuestMode()) return;
|
||||||
if (!saveProjectNameInput) return;
|
if (!saveProjectNameInput) return;
|
||||||
const trimmedName = saveProjectNameInput.value.trim();
|
const trimmedName = saveProjectNameInput.value.trim();
|
||||||
if (!trimmedName) {
|
if (!trimmedName) {
|
||||||
|
|
@ -3706,11 +3868,17 @@ const loadProjectSnapshot = async (project) => {
|
||||||
}
|
}
|
||||||
inspectorCollapseButton?.addEventListener("click", () => setInspectorCollapsed(true));
|
inspectorCollapseButton?.addEventListener("click", () => setInspectorCollapsed(true));
|
||||||
inspectorRestoreButton?.addEventListener("click", () => setInspectorCollapsed(false));
|
inspectorRestoreButton?.addEventListener("click", () => setInspectorCollapsed(false));
|
||||||
loadProjectsList().catch(() => {
|
if (!isGuestMode()) {
|
||||||
templatesMenu?.setData({ templates: templateFallbacks, projects: [], error: "API недоступно" });
|
loadProjectsList().catch(() => {
|
||||||
});
|
templatesMenu?.setData({ templates: templateFallbacks, projects: [], error: "API недоступно" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handleFile = (file) => {
|
const handleFile = (file) => {
|
||||||
|
if (isGuestMode()) {
|
||||||
|
setStatus("Гостевой просмотр не принимает загрузку файлов");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const detected = guessTypeFromName(file.name);
|
const detected = guessTypeFromName(file.name);
|
||||||
if (!detected) {
|
if (!detected) {
|
||||||
showError("Формат файла не поддерживается. Выберите .xkt, .glb, .gltf, .bim, .las, .laz, .obj, .stl");
|
showError("Формат файла не поддерживается. Выберите .xkt, .glb, .gltf, .bim, .las, .laz, .obj, .stl");
|
||||||
|
|
@ -3744,6 +3912,7 @@ const loadProjectSnapshot = async (project) => {
|
||||||
if (viewerWrapper) {
|
if (viewerWrapper) {
|
||||||
["dragenter", "dragover"].forEach((evt) => {
|
["dragenter", "dragover"].forEach((evt) => {
|
||||||
viewerWrapper.addEventListener(evt, (e) => {
|
viewerWrapper.addEventListener(evt, (e) => {
|
||||||
|
if (isGuestMode()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dropZone.classList.add("active");
|
dropZone.classList.add("active");
|
||||||
|
|
@ -3752,6 +3921,7 @@ const loadProjectSnapshot = async (project) => {
|
||||||
|
|
||||||
["dragleave", "drop"].forEach((evt) => {
|
["dragleave", "drop"].forEach((evt) => {
|
||||||
viewerWrapper.addEventListener(evt, (e) => {
|
viewerWrapper.addEventListener(evt, (e) => {
|
||||||
|
if (isGuestMode()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dropZone.classList.remove("active");
|
dropZone.classList.remove("active");
|
||||||
|
|
@ -3759,6 +3929,7 @@ const loadProjectSnapshot = async (project) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
viewerWrapper.addEventListener("drop", (e) => {
|
viewerWrapper.addEventListener("drop", (e) => {
|
||||||
|
if (isGuestMode()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const file = e.dataTransfer?.files?.[0];
|
const file = e.dataTransfer?.files?.[0];
|
||||||
|
|
@ -3862,10 +4033,24 @@ const loadProjectSnapshot = async (project) => {
|
||||||
closeDeleteModal();
|
closeDeleteModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
shareModalCancel?.addEventListener("click", closeShareModal);
|
||||||
|
shareModalClose?.addEventListener("click", closeShareModal);
|
||||||
|
shareCopyButton?.addEventListener("click", copyShareLink);
|
||||||
|
shareModalBackdrop?.addEventListener("click", (e) => {
|
||||||
|
if (e.target === shareModalBackdrop) {
|
||||||
|
closeShareModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key !== "Escape") return;
|
if (e.key !== "Escape") return;
|
||||||
if (e.target && ["INPUT", "TEXTAREA", "SELECT"].includes(e.target.tagName)) return;
|
if (e.target && ["INPUT", "TEXTAREA", "SELECT"].includes(e.target.tagName)) return;
|
||||||
|
if (shareModal && !shareModal.classList.contains("hidden")) {
|
||||||
|
closeShareModal();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (deleteProjectModal && !deleteProjectModal.classList.contains("hidden")) {
|
if (deleteProjectModal && !deleteProjectModal.classList.contains("hidden")) {
|
||||||
closeDeleteModal();
|
closeDeleteModal();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -4106,6 +4291,7 @@ const loadProjectSnapshot = async (project) => {
|
||||||
bottomToolbar.addEventListener("click", (e) => {
|
bottomToolbar.addEventListener("click", (e) => {
|
||||||
const btn = e.target.closest(".tb-btn");
|
const btn = e.target.closest(".tb-btn");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
|
if (isGuestMode()) return;
|
||||||
const action = btn.dataset.action;
|
const action = btn.dataset.action;
|
||||||
if (action === "settings") {
|
if (action === "settings") {
|
||||||
if (btn.classList.contains("active")) {
|
if (btn.classList.contains("active")) {
|
||||||
|
|
@ -4124,6 +4310,8 @@ const loadProjectSnapshot = async (project) => {
|
||||||
setMeasureActive(!measureActive);
|
setMeasureActive(!measureActive);
|
||||||
} else if (action === "projection") {
|
} else if (action === "projection") {
|
||||||
toggleProjection();
|
toggleProjection();
|
||||||
|
} else if (action === "share") {
|
||||||
|
openShareModal();
|
||||||
} else if (action === "add") {
|
} else if (action === "add") {
|
||||||
fileInput.click();
|
fileInput.click();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="./dcViewer.css?v=16">
|
<link rel="stylesheet" href="./dcViewer.css?v=17">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
|
@ -220,7 +220,11 @@
|
||||||
<div id="saveProjectModal" class="save-project-modal hidden">
|
<div id="saveProjectModal" class="save-project-modal hidden">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title">Сохранить проект</div>
|
<div class="modal-title">Сохранить проект</div>
|
||||||
<button class="modal-close" id="saveProjectClose" aria-label="Закрыть">×</button>
|
<button class="modal-close inspector-collapse-btn" id="saveProjectClose" aria-label="Закрыть">
|
||||||
|
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
||||||
|
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<label for="saveProjectName" class="input-label">Имя проекта</label>
|
<label for="saveProjectName" class="input-label">Имя проекта</label>
|
||||||
<input type="text" id="saveProjectName" placeholder="Новый проект" autocomplete="off">
|
<input type="text" id="saveProjectName" placeholder="Новый проект" autocomplete="off">
|
||||||
|
|
@ -236,7 +240,11 @@
|
||||||
<div id="deleteProjectModal" class="save-project-modal hidden">
|
<div id="deleteProjectModal" class="save-project-modal hidden">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title">Удалить проект</div>
|
<div class="modal-title">Удалить проект</div>
|
||||||
<button class="modal-close" id="deleteProjectClose" aria-label="Закрыть">×</button>
|
<button class="modal-close inspector-collapse-btn" id="deleteProjectClose" aria-label="Закрыть">
|
||||||
|
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
||||||
|
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-hint">Вы уверены, что хотите удалить проект?</div>
|
<div class="input-hint">Вы уверены, что хотите удалить проект?</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
|
|
@ -245,18 +253,64 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="error-text" id="deleteProjectError"></div>
|
<div class="error-text" id="deleteProjectError"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="shareModalBackdrop" class="modal-backdrop hidden"></div>
|
||||||
|
<div id="shareModal" class="save-project-modal hidden">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">Ссылка на модель</div>
|
||||||
|
<button class="modal-close inspector-collapse-btn" id="shareModalClose" aria-label="Закрыть">
|
||||||
|
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
||||||
|
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="shareLinkInput" readonly autocomplete="off">
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="secondary-btn" id="shareModalCancel" type="button">Закрыть</button>
|
||||||
|
<button class="primary-btn" id="shareCopyButton" type="button" disabled>Скопировать</button>
|
||||||
|
</div>
|
||||||
|
<div class="error-text" id="shareError"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="bottomToolbar" class="bottom-toolbar">
|
<div id="bottomToolbar" class="bottom-toolbar">
|
||||||
<button class="tb-btn" data-action="settings" title="Настройки">⚙</button>
|
<button class="tb-btn" data-action="settings" title="Настройки" aria-label="Настройки">
|
||||||
<button class="tb-btn" data-action="templates" title="Примеры">📂</button>
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<button class="tb-btn" data-action="measure" title="Линейка">📏</button>
|
<path d="M12 15.5A3.5 3.5 0 1 0 12 8a3.5 3.5 0 0 0 0 7.5Z"></path>
|
||||||
|
<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.37a1.7 1.7 0 0 0-1 .56V20a2 2 0 0 1-4 0v-.08a1.7 1.7 0 0 0-1-.56 1.7 1.7 0 0 0-1.88.34l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.63 15a1.7 1.7 0 0 0-.56-1H4a2 2 0 0 1 0-4h.08a1.7 1.7 0 0 0 .56-1 1.7 1.7 0 0 0-.34-1.88l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.63a1.7 1.7 0 0 0 1-.56V4a2 2 0 0 1 4 0v.08a1.7 1.7 0 0 0 1 .56 1.7 1.7 0 0 0 1.88-.34l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.37 9c.18.38.38.73.56 1H20a2 2 0 0 1 0 4h-.08c-.18.27-.37.62-.52 1Z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="tb-btn" data-action="templates" title="Примеры" aria-label="Примеры">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M3.5 6.5h6l2 2h9v8.75a2.25 2.25 0 0 1-2.25 2.25H5.75A2.25 2.25 0 0 1 3.5 17.25V6.5Z"></path>
|
||||||
|
<path d="M3.5 10h17"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="tb-btn" data-action="measure" title="Линейка" aria-label="Линейка">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M4.5 15.5 15.5 4.5l4 4-11 11-4-4Z"></path>
|
||||||
|
<path d="m8 12 2 2"></path>
|
||||||
|
<path d="m10.5 9.5 1.5 1.5"></path>
|
||||||
|
<path d="m13 7 2 2"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button class="tb-btn" data-action="projection" title="Перспектива / аксонометрия">A</button>
|
<button class="tb-btn" data-action="projection" title="Перспектива / аксонометрия">A</button>
|
||||||
<button class="tb-btn" data-action="add" title="Добавить файл">+</button>
|
<button class="tb-btn" data-action="share" title="Поделиться моделью" aria-label="Поделиться моделью">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 14.5V4.75"></path>
|
||||||
|
<path d="m8.25 8.5 3.75-3.75 3.75 3.75"></path>
|
||||||
|
<path d="M7.25 11.25H6.5a2 2 0 0 0-2 2v4.25a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-4.25a2 2 0 0 0-2-2h-.75"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="tb-btn" data-action="add" title="Добавить файл" aria-label="Добавить файл">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 5v14"></path>
|
||||||
|
<path d="M5 12h14"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input id="fileInput" class="hidden" type="file"
|
<input id="fileInput" class="hidden" type="file"
|
||||||
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
|
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
|
||||||
|
|
||||||
<script type="module" src="./dcViewer.js?v=21"></script>
|
<script type="module" src="./dcViewer.js?v=22"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,17 @@
|
||||||
|
|
||||||
Статика: раздаётся из `../frontend/dist`, при отсутствии сборки — из `../frontend`.
|
Статика: раздаётся из `../frontend/dist`, при отсутствии сборки — из `../frontend`.
|
||||||
|
|
||||||
Хранилище: `server/data/projects/index.json` (список) + `server/data/projects/proj_<id>.json` (полный проект). При старте создаются при необходимости.
|
Хранилище: `server/data/projects/index.json` (список) + `server/data/projects/proj_<id>.json` (полный проект).
|
||||||
|
Upload-артефакты лежат в `server/data/uploads`, публичные share-ссылки — в `server/data/shares/index.json`,
|
||||||
|
а пользовательские ссылки/ref-count моделей — в `server/data/models/registry.json`. При старте директории и индексы создаются при необходимости.
|
||||||
|
|
||||||
API:
|
API:
|
||||||
- `GET /api/projects[?type=project|template]` — укороченный список из `index.json`.
|
- `GET /api/projects[?type=project|template]` — укороченный список из `index.json`.
|
||||||
- `GET /api/projects/:id` — полный JSON проекта.
|
- `GET /api/projects/:id` — полный JSON проекта.
|
||||||
- `POST /api/projects` — создать проект, генерируется `id`, `createdAt`, `updatedAt`, сохраняется файл и обновляется индекс.
|
- `POST /api/projects` — создать проект, генерируется `id`, `createdAt`, `updatedAt`, сохраняется файл и обновляется индекс.
|
||||||
- `POST /api/uploads?filename=<name>&projectId=<id>` — сохранить оригинальный файл модели в `server/data/uploads`.
|
- `POST /api/uploads?filename=<name>&projectId=<id>` — сохранить оригинальный файл модели в `server/data/uploads`.
|
||||||
|
- `DELETE /api/uploads/asset` — снять ссылку текущего пользователя на модель и физически удалить asset только если больше нет user/share refs.
|
||||||
|
- `DELETE /api/uploads/version` — удалить версию, если на неё нет чужих refs или публичных share-ссылок.
|
||||||
- `GET /api/conversions/status?src=<uploads/...>` — получить статус подготовки viewer-артефакта.
|
- `GET /api/conversions/status?src=<uploads/...>` — получить статус подготовки viewer-артефакта.
|
||||||
Для `.step/.stp` ответ помечается `conversion.status=conversion_required`: оригинал уже доступен для скачивания,
|
Для `.step/.stp` ответ помечается `conversion.status=conversion_required`: оригинал уже доступен для скачивания,
|
||||||
а preview должен появиться после работы `NodeDcBimConverter`, который готовит GLB и metadata/tree.
|
а preview должен появиться после работы `NodeDcBimConverter`, который готовит GLB и metadata/tree.
|
||||||
|
|
@ -25,3 +29,15 @@ API:
|
||||||
- `docker compose -f docker-compose.beam.yml up nodedc-bim-converter`
|
- `docker compose -f docker-compose.beam.yml up nodedc-bim-converter`
|
||||||
|
|
||||||
Поле `ownerId` пока всегда `null`, но оставлено для будущих пользователей/шаринга.
|
Поле `ownerId` пока всегда `null`, но оставлено для будущих пользователей/шаринга.
|
||||||
|
|
||||||
|
NODE.DC auth/share:
|
||||||
|
- `NODEDC_BIM_AUTH_REQUIRED=1` включает вход через Launcher handoff.
|
||||||
|
- `NODEDC_INTERNAL_ACCESS_TOKEN` должен совпадать с Launcher internal token.
|
||||||
|
- `NODEDC_LAUNCHER_BASE_URL` — публичный Launcher URL для редиректа на login/launch.
|
||||||
|
- `NODEDC_LAUNCHER_INTERNAL_URL` — внутренний URL Launcher для `/api/internal/handoff/consume`.
|
||||||
|
- `NODEDC_BIM_PUBLIC_URL` — публичный URL viewer-а для share-ссылок, на проде `https://bim.nodedc.ru`.
|
||||||
|
- `NODEDC_BIM_DATA_ROOT` — опциональный корень файлового storage вместо `server/data`.
|
||||||
|
- `POST /api/shares` создаёт публичную read-only ссылку на upload artifact; `/share/<token>` открывает viewer в guest mode.
|
||||||
|
- `GET /api/models` возвращает личные активные модели текущего пользователя.
|
||||||
|
- `POST /api/models/refs` сохраняет существующий upload artifact в личные модели текущего пользователя.
|
||||||
|
- `DELETE /api/models/refs` снимает личную ссылку и удаляет физические файлы только при отсутствии других refs/share.
|
||||||
|
|
|
||||||
2034
server/index.js
2034
server/index.js
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue