screentinker/server/routes
ScreenTinker b67775283b fix(server): NFC normalize user-facing filenames in safeFilename
Single line change to safeFilename() in routes/content.js: add
.normalize('NFC') before sanitizeString. Covers all 4 user-facing
filename storage sites (POST /, POST /remote, POST /embed, PUT /:id
rename) since they all flow through safeFilename.

Fixes macOS NFD vs Linux NFC mismatch on filename storage that mangled
umlauts (ae/oe/ue/ss) in displayed filenames. macOS clients send
NFD-decomposed names (e.g. 'u' + combining diaeresis U+0308 instead of
the precomposed U+00FC); Linux + most renderers expect NFC. Without
this, names like 'Begruessungsscreens.jpg' arrive with the combining
char floating and display as mojibake.

Reported by semetra22 in Discord with extraordinarily good debugging
narrowing (rename works, upload doesn't = bug is in upload path).
Single-point fix at the convergence of all user-facing filename flows.

Existing NFD-mangled rows in DB not backfilled; users can re-upload or
rename to repair. Optional one-time UPDATE backfill captured as follow-up
in handoff doc.

Smoke verified by invoking safeFilename directly on NFD + NFC inputs of
'Begruessungsscreens.jpg' - both produce identical NFC-normalized bytes
(42656772c3bcc39f756e677373637265656e732e6a7067).
2026-05-12 11:51:34 -05:00
..
activity.js Phase 2.1: tenancy middleware, permission helpers, JWT workspace context, frontend + backend role-rename compat 2026-05-11 20:02:00 -05:00
assignments.js Phase 2.2j: assignments.js scoped to workspace_id via playlist.workspace_id; fixes 3 pre-existing cross-tenant leaks (content add, widget add with NO existing check, copy-to cross-workspace); ensureDevicePlaylist loop-closer; status.js playlist INSERT bundle 2026-05-11 22:12:13 -05:00
auth.js feat(workspaces): rename via switcher dropdown - new PATCH /api/workspaces/:id route, per-row pencil affordance in switcher (visible only when caller can_admin), small rename modal with name + slug fields, validation (name <=80 chars, slug ^[a-z0-9]+(?:-[a-z0-9]+)*$ <=60 chars, blank slug -> NULL), 409 on per-org slug collision. Permission gating via new canAdminWorkspace(db, user, ws) helper in lib/permissions.js - reused-ready for future Phase 3 admin actions. /me query now joins organization_members to compute can_admin per accessible_workspaces entry. Drive-by fixes surfaced: (1) activityLogger method filter was missing PATCH, added; (2) routes that operate on a target workspace by URL param need to stamp req.workspaceId from the param so activityLogger captures the right tenant attribution - documented in the route. Smoke fixture: switcher-test@local.test is workspace_admin of Studio A and workspace_editor of Field Crew (no org_owner) so the can_admin true/false split is exercised in one login. 2026-05-12 11:06:55 -05:00
content.js fix(server): NFC normalize user-facing filenames in safeFilename 2026-05-12 11:51:34 -05:00
device-groups.js Phase 2.2i: device-groups.js scoped to workspace_id; fixes 3 pre-existing cross-tenant leaks (group device add, bulk content assign, bulk playlist assign); pre-emptive workspace_id stamp on ensureDevicePlaylist helper 2026-05-11 21:58:13 -05:00
devices.js feat(socket): Phase 2.3 workspace-scoped dashboard socket rooms + per-command permission gates. Dashboard namespace was previously a flat broadcast - every connected dashboard received every device's status/screenshot/playback events platform-wide (foreign device names + IPs included). Inbound socket commands gated by a legacy admin/superadmin role check that was dead code post-Phase-1 rename. 2026-05-12 11:34:24 -05:00
folders.js Phase 2.2c: content_folders gets workspace_id (schema + backfill); folders.js scoped; content.js folder-move strict same-workspace 2026-05-11 21:04:03 -05:00
kiosk.js Phase 2.2e: kiosk.js scoped to workspace_id; import kiosk INSERT bundled 2026-05-11 21:20:18 -05:00
layouts.js Phase 2.2h: layouts.js scoped to workspace_id; templates via is_template path; fixes pre-existing PUT /device/:deviceId cross-tenant layout-assignment leak 2026-05-11 21:45:28 -05:00
playlists.js fix(playlists): POST /publish returns items with pi.id so post-publish delete works without refresh; future refactor candidate to share SELECT shape with GET /:id 2026-05-11 22:28:59 -05:00
provisioning.js Initial open source release 2026-04-08 12:14:53 -05:00
reports.js Phase 2.2g: reports.js scoped to workspace_id; fixes pre-existing /export and /uptime cross-tenant leaks 2026-05-11 21:36:54 -05:00
schedules.js Phase 2.2m: schedules.js scoped to workspace_id; schedule.workspace_id inherited from target (device/group); fixes 6 pre-existing cross-tenant leaks (POST content/widget/layout/playlist accepted with no check, PUT verifyOwnership rewrite across all 6 polymorphic targets) 2026-05-11 23:03:54 -05:00
status.js Phase 2.2j: assignments.js scoped to workspace_id via playlist.workspace_id; fixes 3 pre-existing cross-tenant leaks (content add, widget add with NO existing check, copy-to cross-workspace); ensureDevicePlaylist loop-closer; status.js playlist INSERT bundle 2026-05-11 22:12:13 -05:00
stripe.js Security audit remediation: auth, IDOR, XSS, hardening 2026-04-11 22:48:07 -05:00
subscription.js Initial open source release 2026-04-08 12:14:53 -05:00
teams.js Phase 2.1: tenancy middleware, permission helpers, JWT workspace context, frontend + backend role-rename compat 2026-05-11 20:02:00 -05:00
video-walls.js feat(socket): Phase 2.3 workspace-scoped dashboard socket rooms + per-command permission gates. Dashboard namespace was previously a flat broadcast - every connected dashboard received every device's status/screenshot/playback events platform-wide (foreign device names + IPs included). Inbound socket commands gated by a legacy admin/superadmin role check that was dead code post-Phase-1 rename. 2026-05-12 11:34:24 -05:00
white-label.js Phase 2.2f: white-label.js scoped to workspace_id; requireWorkspaceAdmin gate; status.js bundle 2026-05-11 21:30:22 -05:00
widgets.js Phase 2.2d: widgets.js scoped to workspace_id; import + widget-reference defense bundled 2026-05-11 21:13:51 -05:00
workspaces.js feat(workspaces): rename via switcher dropdown - new PATCH /api/workspaces/:id route, per-row pencil affordance in switcher (visible only when caller can_admin), small rename modal with name + slug fields, validation (name <=80 chars, slug ^[a-z0-9]+(?:-[a-z0-9]+)*$ <=60 chars, blank slug -> NULL), 409 on per-org slug collision. Permission gating via new canAdminWorkspace(db, user, ws) helper in lib/permissions.js - reused-ready for future Phase 3 admin actions. /me query now joins organization_members to compute can_admin per accessible_workspaces entry. Drive-by fixes surfaced: (1) activityLogger method filter was missing PATCH, added; (2) routes that operate on a target workspace by URL param need to stamp req.workspaceId from the param so activityLogger captures the right tenant attribution - documented in the route. Smoke fixture: switcher-test@local.test is workspace_admin of Studio A and workspace_editor of Field Crew (no org_owner) so the can_admin true/false split is exercised in one login. 2026-05-12 11:06:55 -05:00