Sync BIM login state and stabilize zoom

This commit is contained in:
CODEX 2026-06-23 19:28:47 +03:00
parent ce34ecdd4d
commit 249d46c7c7
2 changed files with 65 additions and 2 deletions

View File

@ -447,9 +447,10 @@ const configureCameraControl = () => {
if (!control) return;
activeZoomSpeed = normalizeZoomSpeed(activeZoomSpeed);
configureCameraPanMap(control);
control.followPointer = true;
// Keep wheel dolly speed independent from whether the pointer is over model geometry.
control.followPointer = false;
control.doublePickFlyTo = false;
control.smartPivot = true;
control.smartPivot = false;
control.zoomOnMouseWheel = true;
control.keyboardDollyRate = CAMERA_CONTROL_SETTINGS.keyboardDollyRate;
control.mouseWheelDollyRate = activeZoomSpeed;
@ -684,6 +685,8 @@ let nodeDCLogoutHandling = false;
let nodeDCLoginHandling = false;
let nodeDCSessionWatchTimer = null;
let nodeDCSessionWatchInFlight = false;
let nodeDCLoginSyncPrimed = false;
let nodeDCLoginSyncEventId = "";
const handleNodeDCSessionLogout = async () => {
if (nodeDCLogoutHandling) return;
@ -844,9 +847,24 @@ const scheduleShareSessionPromotion = (shareToken) => {
document.body.appendChild(iframe);
};
const getLoginSyncEventId = (sessionPayload) => (
typeof sessionPayload?.loginSyncEventId === "string" ? sessionPayload.loginSyncEventId : ""
);
const applyLiveSessionPayload = async (sessionPayload, { reason = "watch" } = {}) => {
const wasAuthenticated = runtimeSessionInfo.authenticated === true;
const isAuthenticated = sessionPayload?.authenticated === true;
const loginSyncEventId = getLoginSyncEventId(sessionPayload);
const hasNewLoginSyncEvent =
nodeDCLoginSyncPrimed &&
!isAuthenticated &&
!!loginSyncEventId &&
loginSyncEventId !== nodeDCLoginSyncEventId;
nodeDCLoginSyncPrimed = true;
if (loginSyncEventId) {
nodeDCLoginSyncEventId = loginSyncEventId;
}
runtimeSessionInfo = {
...runtimeSessionInfo,
@ -861,6 +879,10 @@ const applyLiveSessionPayload = async (sessionPayload, { reason = "watch" } = {}
await handleNodeDCSessionLogout();
return;
}
if (hasNewLoginSyncEvent) {
handleNodeDCSessionLogin();
return;
}
const shareToken = getShareTokenFromPath();
if (shareToken && viewerMode === "guest") {
scheduleShareSessionPromotion(shareToken);

View File

@ -56,6 +56,7 @@ const BIM_SESSION_COOKIE = process.env.NODEDC_BIM_SESSION_COOKIE || "nodedc_bim_
const BIM_HANDOFF_COOKIE = process.env.NODEDC_BIM_HANDOFF_COOKIE || "nodedc_bim_handoff";
const BIM_SESSION_TTL_MS = Number(process.env.NODEDC_BIM_SESSION_TTL_MS || 12 * 60 * 60 * 1000);
const BIM_HANDOFF_TTL_SECONDS = Number(process.env.NODEDC_BIM_HANDOFF_TTL_SECONDS || 60);
const BIM_LOGIN_SYNC_TTL_MS = Number(process.env.NODEDC_BIM_LOGIN_SYNC_TTL_MS || 60 * 1000);
const BIM_COOKIE_SECURE = parseBooleanEnv(process.env.NODEDC_BIM_COOKIE_SECURE, false);
const BIM_COOKIE_SAMESITE = process.env.NODEDC_BIM_COOKIE_SAMESITE || (BIM_COOKIE_SECURE ? "None" : "Lax");
const SHARE_AUTH_GUEST_PARAM = "ndc_bim_guest";
@ -64,6 +65,7 @@ const CORS_ALLOWED_ORIGINS = String(process.env.NODEDC_BIM_ALLOWED_ORIGINS || ""
.map((origin) => origin.trim())
.filter(Boolean);
const bimSessions = new Map();
let latestBimLoginSyncEvent = null;
const MIME_TYPES = {
".html": "text/html",
@ -721,6 +723,27 @@ const parseJSONBody = async (req) => {
const randomBase64Url = (bytes = 24) => crypto.randomBytes(bytes).toString("base64url");
const markBimLoginSyncEvent = () => {
const ttlMs = Number.isFinite(BIM_LOGIN_SYNC_TTL_MS) ? Math.max(1000, BIM_LOGIN_SYNC_TTL_MS) : 60 * 1000;
latestBimLoginSyncEvent = {
id: randomBase64Url(16),
createdAt: nowIso(),
expiresAt: Date.now() + ttlMs
};
return latestBimLoginSyncEvent;
};
const getActiveBimLoginSyncEvent = () => {
if (!latestBimLoginSyncEvent) {
return null;
}
if (latestBimLoginSyncEvent.expiresAt <= Date.now()) {
latestBimLoginSyncEvent = null;
return null;
}
return latestBimLoginSyncEvent;
};
const sanitizeReturnTo = (value, fallback = "/") => {
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//")) {
return fallback;
@ -1749,16 +1772,30 @@ const handleGetUploadVersions = async (req, res, searchParams) => {
const handleAuthSession = async (req, res) => {
const session = await getValidatedBimSession(req, res);
const loginSyncEvent = getActiveBimLoginSyncEvent();
res.setHeader("Cache-Control", "no-store");
sendJSON(res, 200, {
ok: true,
authRequired: BIM_AUTH_REQUIRED,
authenticated: !!session,
canShare: !BIM_AUTH_REQUIRED || !!session,
user: session?.user || null,
loginSyncEventId: loginSyncEvent?.id || null,
loginSyncEventCreatedAt: loginSyncEvent?.createdAt || null,
loginUrl: session ? null : buildLauncherLoginUrl(req, "/")
});
};
const handleLoginSync = (res) => {
const event = markBimLoginSyncEvent();
setCors(res);
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.setHeader("Cache-Control", "no-store");
res.setHeader("X-NODEDC-Session-Event", event.id);
res.end("ok");
};
const handleLauncherHandoff = async (req, res, url) => {
const token = url.searchParams.get("token") || "";
const nextPath = sanitizeReturnTo(url.searchParams.get("next_path") || url.searchParams.get("returnTo") || "/");
@ -3435,6 +3472,10 @@ const requestHandler = async (req, res) => {
return handleSharePromoted(res, url);
}
if (req.method === "GET" && url.pathname === "/auth/login-sync") {
return handleLoginSync(res);
}
if (req.method === "GET" && url.pathname === "/auth/logout") {
return handleLogout(req, res);
}