mirror of
https://github.com/ruvnet/RuView
synced 2026-07-27 18:11:43 +00:00
fix(sensing-server): exempt /api/v1/stream/pose WS from bearer auth; add UI token field
Browsers cannot attach an Authorization header to a WebSocket upgrade, so with RUVIEW_API_TOKEN set the Live Demo pose stream at /api/v1/stream/pose always failed with 401 — the same reason /ws/sensing is already exempted (see bearer_auth module docs). Adds a narrow EXEMPT_PATHS list plus a regression test that the exemption does not leak to other /api/v1/* paths. Query-string tokens remain rejected (CWE-598 test untouched). Also adds an 'API Access' bearer-token field to the QuickSettings panel: ui/services/api.service.js had setAuthToken() but nothing ever called it, so enabling RUVIEW_API_TOKEN broke every /api/v1/* call from the bundled dashboard. The token is stored in localStorage and applied before the first request. Fixes #1310 Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
// Quick Settings Panel - Centralized configuration for all UI features
|
||||
// Accessible via gear icon in header
|
||||
|
||||
import { apiService } from '../services/api.service.js';
|
||||
|
||||
const API_TOKEN_STORAGE_KEY = 'ruview-api-token';
|
||||
|
||||
export class QuickSettings {
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
@@ -10,10 +14,21 @@ export class QuickSettings {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.applyStoredApiToken();
|
||||
this.createButton();
|
||||
this.createPanel();
|
||||
}
|
||||
|
||||
// Apply a previously-saved bearer token to apiService as early as
|
||||
// possible, before any tab's REST calls fire. The server only ever
|
||||
// checks the `Authorization: Bearer` header (see bearer_auth.rs) — this
|
||||
// intentionally never puts the token in a URL query string.
|
||||
applyStoredApiToken() {
|
||||
let token = null;
|
||||
try { token = localStorage.getItem(API_TOKEN_STORAGE_KEY); } catch { /* noop */ }
|
||||
if (token) apiService.setAuthToken(token);
|
||||
}
|
||||
|
||||
createButton() {
|
||||
this.button = document.createElement('button');
|
||||
this.button.className = 'settings-gear';
|
||||
@@ -70,6 +85,18 @@ export class QuickSettings {
|
||||
<span class="qs-switch"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="qs-section">
|
||||
<div class="qs-section-title">API Access</div>
|
||||
<div class="qs-row" style="flex-direction: column; align-items: stretch; gap: 6px;">
|
||||
<span>Bearer token (set only if the server enforces RUVIEW_API_TOKEN)</span>
|
||||
<input type="password" id="qs-api-token" class="qs-text-input" placeholder="Paste token..." autocomplete="off" style="width: 100%; box-sizing: border-box;">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="qs-btn" id="qs-api-token-save">Save & Apply</button>
|
||||
<button class="qs-btn-danger" id="qs-api-token-clear">Clear</button>
|
||||
</div>
|
||||
<span id="qs-api-token-status" style="font-size: 0.85em; opacity: 0.75;"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qs-section">
|
||||
<div class="qs-section-title">Data</div>
|
||||
<div class="qs-row">
|
||||
@@ -112,6 +139,30 @@ export class QuickSettings {
|
||||
}
|
||||
});
|
||||
|
||||
this.panel.querySelector('#qs-api-token-save').addEventListener('click', () => {
|
||||
const input = this.panel.querySelector('#qs-api-token');
|
||||
const status = this.panel.querySelector('#qs-api-token-status');
|
||||
const token = input.value.trim();
|
||||
if (!token) {
|
||||
status.textContent = 'Enter a token first, or use Clear to remove one.';
|
||||
return;
|
||||
}
|
||||
try { localStorage.setItem(API_TOKEN_STORAGE_KEY, token); } catch { /* noop */ }
|
||||
apiService.setAuthToken(token);
|
||||
status.textContent = 'Token saved and applied. Reloading...';
|
||||
setTimeout(() => window.location.reload(), 600);
|
||||
});
|
||||
|
||||
this.panel.querySelector('#qs-api-token-clear').addEventListener('click', () => {
|
||||
const input = this.panel.querySelector('#qs-api-token');
|
||||
const status = this.panel.querySelector('#qs-api-token-status');
|
||||
try { localStorage.removeItem(API_TOKEN_STORAGE_KEY); } catch { /* noop */ }
|
||||
apiService.setAuthToken(null);
|
||||
input.value = '';
|
||||
status.textContent = 'Token cleared. Reloading...';
|
||||
setTimeout(() => window.location.reload(), 600);
|
||||
});
|
||||
|
||||
this.panel.querySelector('#qs-clear-data').addEventListener('click', () => {
|
||||
try {
|
||||
localStorage.clear();
|
||||
@@ -154,6 +205,10 @@ export class QuickSettings {
|
||||
if (this.getSetting('compact')) {
|
||||
document.body.classList.add('compact-mode');
|
||||
}
|
||||
const status = this.panel.querySelector('#qs-api-token-status');
|
||||
let hasToken = false;
|
||||
try { hasToken = !!localStorage.getItem(API_TOKEN_STORAGE_KEY); } catch { /* noop */ }
|
||||
if (status) status.textContent = hasToken ? 'A token is currently set.' : 'No token set (auth is off or unnecessary).';
|
||||
}
|
||||
|
||||
prefersReducedMotion() {
|
||||
|
||||
Reference in New Issue
Block a user