Skip to content

Multi-display campaigns, secrets management, and admin UI redesign#1

Open
christian-andersson wants to merge 90 commits intomainfrom
update
Open

Multi-display campaigns, secrets management, and admin UI redesign#1
christian-andersson wants to merge 90 commits intomainfrom
update

Conversation

@christian-andersson
Copy link
Member

Summary

Multi-display campaign assignment with per-monitor Chrome instances, encrypted secrets management, and comprehensive admin UI improvements.

Multi-Display Support

  • Replace single campaignId on devices with device_display_campaigns junction table — each display gets its own campaign
  • Launch one Chrome instance per connected display, positioned on the correct screen using OS coordinates
  • macOS: Swift/NSScreen-based detection with EDID hardware IDs (vendor:model:serial) for stable identification across reboots and cable swaps
  • Linux: DRM + xrandr detection with connector-based names (HDMI-1, HDMI-2)
  • Content endpoint returns per-display campaign data, client matches by hardware ID first
  • Admin UI configure modal shows one campaign dropdown per detected display
  • Clear All Campaigns button for quick cleanup of stale assignments

Per-Item Cookies & Headers via CDP

  • Playlist items can have cookies (dismiss cookie banners) and HTTP headers (e.g. Authorization for Grafana)
  • CDP Network.setCookie and Network.setExtraHTTPHeaders applied before each navigation
  • Headers support both plain values and secret references

Encrypted Tenant Secrets

  • New tenant_secrets table with AES-256-GCM encrypted values (ENCRYPTION_KEY env var)
  • Admin/owner-only CRUD API with domain field for URL-scoped enforcement
  • Secret references in playlist item headers resolved server-side — value only sent to matching domain
  • Admin UI secrets page with password-manager-resistant input fields

Auth & UI Fixes

  • Fix email verification token consumed too early (React StrictMode double-fire) — defer deletion to registration completion
  • Fix stale device ID on status page after re-registration
  • Redesign Playlists page with sidebar layout matching Organizations
  • Standardize action buttons (SVG icons, consistent spacing) across all pages
  • Fix long text overflow in sidebar and details panels

Test plan

  • Register user, create org, create campaigns
  • Claim device, assign different campaigns to different displays
  • Verify multi-Chrome launches on correct screens (macOS with 3 displays)
  • Verify campaign content updates via WebSocket push
  • Verify hardware ID based matching survives display name changes
  • Create/edit/delete tenant secrets, verify domain enforcement
  • Add playlist item with secret reference header, verify resolution in content endpoint
  • Verify cookie banner dismissal via per-item cookies

🤖 Generated with Claude Code

christian-andersson and others added 30 commits March 21, 2025 09:54
Add dynamodb backend
rename the register endpoint to a ping endpoint
Create new registration endpoint
Rewrite the client to be more prettier and also show status and last seen
Introduce two new security middleware layers:
- XSS protection: input sanitization, output encoding, CSP headers,
  field-specific sanitization rules
- Tenant authorization: role-based access control (Owner/Admin/Member),
  cached membership validation with 5-min TTL

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Apply tenant authorization middleware to all tenant-specific routes
  (devices, playlists, playlist groups, tenants)
- Add role-based guards: owner-only delete, admin-only invite/role change
- Gate debug endpoints behind NODE_ENV === 'development'
- Add secure session secret validation (reject weak defaults in production)
- Integrate helmet, XSS sanitization, and output encoding into server
- Enhance input validation with XSS protection in validators and controllers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server: add helmet for security headers
Client: add @babel/plugin-proposal-private-property-in-object as
explicit devDependency to fix deprecation warning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update CLAUDE.md with session security, XSS, and tenant auth guidelines
- Add server/SECURITY-SESSION.md for session configuration reference
- Add server/TENANT-AUTHORIZATION.md for tenant auth architecture
- Add server/XSS-PROTECTION.md for XSS prevention documentation
- Add DEPENDENCY-SECURITY.md for dependency security policy
- Add server/.env.example as configuration template (no real secrets)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- actions/checkout: v4 -> v6.0.2 (SHA-pinned)
- docker/setup-buildx-action: v1 -> v3.12.0 (SHA-pinned)
- docker/login-action: v1 -> v3.7.0 (SHA-pinned)
- docker/build-push-action: v2 -> v6.19.2 (SHA-pinned)

SHA-pinning prevents supply chain attacks via mutable version tags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent internal audit/task files and local Claude settings
from being tracked in version control.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ownership is now determined solely by tenant_members.role = 'owner'
instead of a dedicated ownerId column on the tenants table. This
enables multiple owners and a cleaner permission model:
- Owner: full control, can manage all roles, delete empty orgs
- Admin: can manage admins and members, cannot touch owners
- Member: view only
Previously, the user account was created in completeRegistration before
the passkey step. If passkey creation failed, the user was left with no
way to log in. Now completeRegistration only stores pending data in the
session and returns WebAuthn options directly. The actual user record,
authenticator, and tenant setup are created atomically in the new
/webauthn/register-new endpoint after passkey verification succeeds.
…rving

- Consume email verification tokens on first use to prevent reuse
- Use configured env.ORIGIN for verification links instead of attacker-controlled Origin/Referer headers
- Canonicalize static file paths with Deno.realPath and prefix check to prevent directory traversal
TypeScript-based provisioner that SSHs into a Raspberry Pi and configures
it as a digital signage device. Follows the same ask/save/do step pattern
as the original bash installer but written in Deno/TypeScript.

Steps: init, OS upgrade, X11/Chromium kiosk, WiFi, OpenFortiVPN, Deno
install, signage-client deploy with systemd service. Config answers are
saved locally for batch-provisioning multiple devices.

Usage: cd signage-client && deno task provision --host=192.168.1.50
Auto-detect Raspberry Pi generation, HDMI ports, boot config path,
KMS mode, and WiFi availability via SSH on connect. Steps now adapt:
- Boot config: /boot/config.txt (Pi 3/4) vs /boot/firmware/config.txt (Pi 5)
- KMS overlay: fake KMS for Pi 3, full KMS for Pi 4/5
- Display: single xrandr for Pi 3 (1 HDMI), dual for Pi 4/5 (2 HDMI)
- WiFi: skip entirely if no wlan0 adapter detected (e.g. Pi 3 Model B)
Detect connected displays at runtime and report to server via heartbeat:
- Linux: DRM subsystem (/sys/class/drm/) with xrandr fallback
- macOS: system_profiler SPDisplaysDataType
- Windows: PowerShell WMI with Win32_DesktopMonitor fallback

Also adds Windows support to Chrome launcher (paths, where command,
temp dir) and display power control (SendMessage API via PowerShell).
Add device_display_campaigns junction table to support assigning different
campaigns to each display on a multi-monitor device (e.g. Pi 4/5 with 2
HDMI ports). Remove campaignId column from devices table.

Update API route to POST .../displays/:displayName/campaign, return
per-display data from GET /api/device/content, and track multiple
campaign IDs per device in WebSocket manager.

Admin UI configure modal now shows one campaign dropdown per detected
display instead of a single global dropdown.
Previously the token was deleted immediately when the verify-email
endpoint was hit, before passkey setup. React StrictMode double-fires
effects in dev, causing the second call to find no token and show an
error. Move deletion to verifyNewRegistration so the token remains
valid through the entire registration flow.
ContentManager now handles the new multi-display response format from
the server, storing campaigns keyed by display name.

Fix player showing old device ID on status page after re-registration
by passing player to handleAuthFailure and calling setDeviceInfo with
the new credentials.
Secrets store sensitive values (API tokens, passwords) that admins can
manage per organization. Values are encrypted at rest using AES-256-GCM
with a server-side ENCRYPTION_KEY. Only tenant owners and admins can
create, update, or delete secrets.

Includes encryption utility, tenant_secrets table with RLS, full CRUD
API behind requireTenantAdmin middleware, and admin UI page.
Replace inconsistent button patterns (emoji, text-only, nested divs)
with uniform btn btn-info/danger btn-sm + SVG icons. Move shared
action-buttons-cell style to common.css. Also move Organizations
"Create" button to header row matching other pages.
Restructure Playlists page to use sidebar + details layout matching
Organizations page. Left panel lists playlists, clicking one shows its
items on the right with header actions.

Also: auto-select first item on Organizations page, fix shared error
state bleeding from modals to page background, fix long names
overflowing sidebar and details panel, use action-buttons-cell
consistently on Organizations header, and deduplicate identical
display names on macOS (e.g. two "LG HDR 4K" → "LG HDR 4K (1/2)").
Replace single-Chrome approach with multi-display architecture. Each
connected display gets its own Chrome instance (positioned via
--window-position from screen coordinates), CDP connection, Player,
and HealthMonitor.

macOS display detection now uses Swift/NSScreen for coordinates and
EDID hardware IDs (vendor:model:serial). Falls back to system_profiler.
Player logs now include display name for easier debugging.
Initial heartbeat sent immediately on startup so server sees displays
before content fetch.
Add hardware_id column to device_display_campaigns so campaign
assignments survive display name changes and cable swaps. Server
stores EDID hardware ID (vendor:model:serial) from the device's
reported displays when assigning campaigns, and keys content
response by hardware ID. Client matches by hardware ID first,
falling back to display name for backward compatibility.
New DELETE /api/device/tenant/:tenantId/devices/:deviceId/campaigns
endpoint clears all display campaign assignments for a device. Admin
UI configure modal shows a "Clear All" button for quick cleanup of
stale assignments after display name changes.
Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants