Files
muxplex/muxplex/frontend/index.html
T
Brian Krabach cb264bbaaa feat: postMessage auth token relay for cross-origin federation login
- Backend: AuthMiddleware.dispatch() checks X-Muxplex-Token header
  after cookie auth, allowing cross-origin federation requests to
  authenticate using a session token sent as a header instead of a
  SameSite=Strict cookie.

- Backend: GET /api/auth/token endpoint returns the current session
  token for authenticated requests. Used by login popups to relay
  the token back to the opener window via postMessage.

- Frontend (app.js): api() injects X-Muxplex-Token header for
  cross-origin requests when a token is stored in localStorage under
  muxplex.federation_tokens keyed by origin.

- Frontend (app.js): storeFederationToken() helper stores tokens.
  window.addEventListener('message') handler catches muxplex-auth-token
  postMessages and stores the token, then retriggers a poll so the
  auth_required source transitions to authenticated immediately.

- Frontend (app.js): _saveRemoteInstances() prunes stale tokens for
  URLs no longer in the remote instances list.

- Frontend (index.html): Popup relay script fetches /api/auth/token
  and sends the token to window.opener via postMessage when loaded
  as a login popup (window.opener present).

Tests added:
  test_auth.py: X-Muxplex-Token header authenticates / invalid rejected
  test_api.py: GET /api/auth/token returns token / 401 when unauthed
  test_app.mjs: api() header injection, storeFederationToken storage,
                index.html popup script presence
2026-04-01 08:17:28 -07:00

253 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#0D1117" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
<link rel="stylesheet" href="/style.css" />
<title>muxplex</title>
</head>
<body>
<script>
// Federation auth: if opened as a login popup by another muxplex instance,
// relay the session token back to the opener window via postMessage.
(function() {
if (!window.opener) return;
fetch('/api/auth/token')
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(data) {
if (data && data.token) {
window.opener.postMessage({
type: 'muxplex-auth-token',
origin: window.location.origin,
token: data.token
}, '*');
setTimeout(function() { window.close(); }, 500);
}
})
.catch(function() {});
})();
</script>
<!-- ── Overview view ──────────────────────────────────────────────────── -->
<div id="view-overview" class="view view--active">
<header class="app-header">
<h1 class="app-wordmark"><img src="/wordmark-on-dark.svg" alt="muxplex" height="24" /></h1>
<div class="header-actions">
<button id="new-session-btn" class="header-btn" aria-label="New session">+</button>
<button id="view-mode-btn" class="header-btn" aria-label="Toggle view mode" title="View: auto">&#9638;</button>
<button id="settings-btn" class="header-btn" aria-label="Settings">&#9881;</button>
<span id="connection-status"></span>
</div>
</header>
<div id="filter-bar" class="filter-bar"></div>
<div id="session-grid" class="session-grid" role="list"></div>
<div id="empty-state" class="empty-state hidden">No active tmux sessions</div>
</div>
<!-- ── Expanded (terminal) view ──────────────────────────────────────── -->
<div id="view-expanded" class="view hidden">
<header class="expanded-header">
<button id="back-btn" class="back-btn" aria-label="Back">&#8592;</button>
<button id="sidebar-toggle-btn" class="sidebar-toggle-btn" aria-label="Toggle session list">&#9776;</button>
<span id="expanded-session-name" class="expanded-session-name"></span>
<button id="settings-btn-expanded" class="header-btn" aria-label="Settings">&#9881;</button>
</header>
<div class="view-body">
<div id="session-sidebar" class="session-sidebar">
<div class="sidebar-header">
<span class="sidebar-title">Sessions</span>
<button id="sidebar-collapse-btn" class="sidebar-collapse-btn" aria-label="Collapse session list">&#8249;</button>
</div>
<div id="sidebar-list" class="sidebar-list"></div>
<div class="sidebar-footer">
<button id="sidebar-new-session-btn" class="sidebar-new-btn">+ New</button>
</div>
</div>
<div id="terminal-container" class="terminal-container"></div>
</div>
<div id="reconnect-overlay" class="reconnect-overlay hidden" aria-live="polite">Reconnecting&hellip;</div>
</div>
<!-- ── Bottom sheet (session switcher) ────────────────────────────────── -->
<div id="bottom-sheet" class="bottom-sheet hidden" role="dialog" aria-modal="true" aria-label="Switch session">
<div class="bottom-sheet__backdrop" id="sheet-backdrop"></div>
<div class="bottom-sheet__panel">
<div class="bottom-sheet__handle" aria-hidden="true"></div>
<ul id="sheet-list" class="bottom-sheet__list" role="listbox" aria-label="Sessions"></ul>
</div>
</div>
<!-- ── Session pill (persistent overlay button) ────────────────────────── -->
<button id="session-pill" class="session-pill hidden" aria-label="Switch session">
<span id="session-pill-label" class="session-pill__label"></span>
<span id="session-pill-bell" class="session-pill__bell hidden" aria-hidden="true">&#128276;</span>
</button>
<!-- ── Mobile FAB (new session) ──────────────────────────────────────────── -->
<button id="new-session-fab" class="new-session-fab" aria-label="New session">+</button>
<!-- ── Toast notification ──────────────────────────────────────────────── -->
<div id="toast" class="toast hidden" role="status" aria-live="polite" aria-atomic="true"></div>
<!-- ── Settings ──────────────────────────────────────────────────────────── -->
<div id="settings-backdrop" class="settings-backdrop hidden"></div>
<dialog id="settings-dialog" class="settings-dialog">
<div class="settings-dialog-header">
<h2 class="settings-dialog-title">Settings</h2>
<button id="settings-close-btn" class="settings-close-btn" aria-label="Close settings">&times;</button>
</div>
<div class="settings-layout">
<nav class="settings-tabs">
<button class="settings-tab settings-tab--active" data-tab="display">Display</button>
<button class="settings-tab" data-tab="sessions">Sessions</button>
<button class="settings-tab" data-tab="new-session">Commands</button>
<button class="settings-tab" data-tab="devices">Multi-Device</button>
</nav>
<div class="settings-content">
<div class="settings-panel" data-tab="display">
<div class="settings-field">
<label class="settings-label" for="setting-device-name">Device Name</label>
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-font-size">Font Size</label>
<select id="setting-font-size" class="settings-select">
<option value="11">11</option>
<option value="12">12</option>
<option value="13">13</option>
<option value="14" selected>14</option>
<option value="16">16</option>
<option value="18">18</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-grid-columns">Grid Columns</label>
<select id="setting-grid-columns" class="settings-select">
<option value="auto" selected>Auto</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-hover-delay">Hover Delay</label>
<select id="setting-hover-delay" class="settings-select">
<option value="0">Off</option>
<option value="1000">1000ms</option>
<option value="1500" selected>1500ms</option>
<option value="2000">2000ms</option>
<option value="3000">3000ms</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-show-hover-preview">Show hover preview</label>
<input type="checkbox" id="setting-show-hover-preview" class="settings-checkbox" checked />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-show-device-badges">Show device badges</label>
<input type="checkbox" id="setting-show-device-badges" class="settings-checkbox" checked />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-activity-indicator">Activity indicator</label>
<select id="setting-activity-indicator" class="settings-select">
<option value="both">Dot + Glow</option>
<option value="glow">Glow only</option>
<option value="dot">Dot only</option>
<option value="none">None</option>
</select>
</div>
</div>
<div class="settings-panel hidden" data-tab="sessions">
<div class="settings-field">
<label class="settings-label" for="setting-default-session">Default Session</label>
<select id="setting-default-session" class="settings-select">
<option value="">(none)</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-sort-order">Sort Order</label>
<select id="setting-sort-order" class="settings-select">
<option value="manual">Manual</option>
<option value="alphabetical">Alphabetical</option>
<option value="recent">Recent</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-window-size-largest">Auto-set window size (largest)</label>
<input type="checkbox" id="setting-window-size-largest" class="settings-checkbox" />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-auto-open">Auto-open created sessions</label>
<input type="checkbox" id="setting-auto-open" class="settings-checkbox" checked />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-bell-sound">Bell Sound</label>
<input type="checkbox" id="setting-bell-sound" class="settings-checkbox" />
</div>
<div class="settings-field">
<label class="settings-label">Desktop Notifications</label>
<div class="settings-notification-status">
<span id="notification-status-text" class="settings-status-text">Not requested</span>
<button id="notification-request-btn" class="settings-action-btn">Request Permission</button>
</div>
</div>
<div class="settings-field settings-field--column">
<label class="settings-label">Hidden Sessions</label>
<div id="setting-hidden-sessions" class="settings-checkbox-list"></div>
</div>
</div>
<div class="settings-panel hidden" data-tab="new-session">
<div class="settings-field settings-field--column">
<label class="settings-label" for="setting-template">Command template</label>
<textarea id="setting-template" class="settings-textarea" rows="3" placeholder="tmux new-session -d -s {name}"></textarea>
<span class="settings-helper">{name} is replaced with the session name</span>
<button id="setting-template-reset" class="settings-action-btn">Reset to default</button>
</div>
<div class="settings-field settings-field--column">
<label class="settings-label" for="setting-delete-template">Delete session command</label>
<textarea id="setting-delete-template" class="settings-textarea" rows="3" placeholder="tmux kill-session -t {name}"></textarea>
<span class="settings-helper">{name} is replaced with the session name</span>
<button id="setting-delete-template-reset" class="settings-action-btn">Reset to default</button>
</div>
</div>
<div class="settings-panel hidden" data-tab="devices">
<div class="settings-field">
<label class="settings-label" for="setting-multi-device-enabled">Enable multi-device support</label>
<input type="checkbox" id="setting-multi-device-enabled" class="settings-checkbox" />
</div>
<div id="multi-device-fields">
<div class="settings-field">
<label class="settings-label" for="setting-view-mode">View Mode</label>
<select id="setting-view-mode" class="settings-select">
<option value="flat">Flat</option>
<option value="grouped">Grouped</option>
<option value="filtered">Filtered</option>
</select>
</div>
<div class="settings-field settings-field--column">
<label class="settings-label">Remote Instances</label>
<div id="setting-remote-instances" class="settings-remote-list"></div>
<button id="add-remote-instance-btn" class="settings-action-btn">+ Add Remote</button>
</div>
</div>
</div>
</div>
</div>
</dialog>
<!-- ── Scripts ──────────────────────────────────────────────────────────── -->
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
<script src="/app.js" defer></script>
<script src="/terminal.js" defer></script>
</body>
</html>