mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Fix: at connect, enumerate the user's accessible workspace_ids (direct workspace_members + org_owner/admin paths + platform_admin 'all') via new accessibleWorkspaceIds() helper in lib/tenancy.js; socket.join one room per workspace. All 12 dashboardNs.emit sites across deviceSocket / heartbeat / server.js / devices route / video-walls route now route via dashboardNs.to(workspaceRoom(...)).emit() with the workspace looked up from the relevant device or wall. New lib/socket-rooms.js holds the helpers and breaks a circular dependency (dashboardSocket already requires heartbeat, so heartbeat can't require dashboardSocket). Inbound 6 commands rewired to canActOnDevice(socket, deviceId, tier): request-screenshot is read tier (workspace_viewer+); remote-touch/key/start/stop and device-command are write tier (workspace_editor+). Platform_admin and org_owner/admin always pass via actingAs. Legacy admin/superadmin branch dropped. Lifecycle note: workspace-switch already calls window.location.reload (Phase 3 switcher), which forces a fresh socket with updated memberships - no per-emit re-evaluation needed. Smoke tested with 3 simultaneous socket.io-client connections (switcher-test, swninja, dw5304 platform_admin) + direct canActOnDevice invocation for 6 user/device/tier combinations. All 9 outbound isolation cells and all 6 permission gates pass. Fixture mutation: switcher-test's Field Crew membership flipped from workspace_editor to workspace_viewer to exercise the read/write tier split in one login.
35 lines
1.3 KiB
JavaScript
35 lines
1.3 KiB
JavaScript
// Phase 2.3: helpers for resolving socket.io room names per workspace /
|
|
// device / wall. Extracted from ws/dashboardSocket.js to break a circular
|
|
// dependency: dashboardSocket already requires services/heartbeat, so
|
|
// heartbeat can't require dashboardSocket. Everything goes through this
|
|
// neutral module instead.
|
|
const { db } = require('../db/database');
|
|
|
|
const ROOM_PREFIX = 'workspace:';
|
|
|
|
function workspaceRoom(workspaceId) {
|
|
return workspaceId ? ROOM_PREFIX + workspaceId : null;
|
|
}
|
|
|
|
function deviceRoom(deviceId) {
|
|
if (!deviceId) return null;
|
|
const d = db.prepare('SELECT workspace_id FROM devices WHERE id = ?').get(deviceId);
|
|
return d?.workspace_id ? workspaceRoom(d.workspace_id) : null;
|
|
}
|
|
|
|
function wallRoom(wallId) {
|
|
if (!wallId) return null;
|
|
const w = db.prepare('SELECT workspace_id FROM video_walls WHERE id = ?').get(wallId);
|
|
return w?.workspace_id ? workspaceRoom(w.workspace_id) : null;
|
|
}
|
|
|
|
// Emit to a workspace room with no-op on missing room. Centralized so callers
|
|
// don't have to remember the "skip if null room" guard - silent drop is safer
|
|
// than the pre-2.3 platform-wide broadcast.
|
|
function emitToWorkspace(ns, room, event, payload) {
|
|
if (!room) return;
|
|
ns.to(room).emit(event, payload);
|
|
}
|
|
|
|
module.exports = { workspaceRoom, deviceRoom, wallRoom, emitToWorkspace };
|