fix: ABS proxy via backend to bypass CORS + settings button layout

- Add /api/abs-proxy/* endpoint in the backend that forwards all ABS API
  calls server-side (X-ABS-URL + X-ABS-Token headers). Browser never
  touches ABS directly for API calls — no CORS issue.
- abs.js routes through proxy when backendUrl is set; falls back to direct
  if no backend (requires user to configure CORS on ABS server)
- Audio streaming still uses direct <audio src> (HTML media bypasses CORS)
- Settings: status text above buttons, buttons full-width flex row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-03 14:44:16 +02:00
1 parent 7c6343e28e
commit d70f649b90
3 files changed
+55 -8

No files matched your search

+31
View File
@@ -381,6 +381,37 @@ app.get('/api/jobs', (req, res) => {
res.json(Array.from(jobs.values()).sort((a, b) => b.createdAt - a.createdAt).slice(0, 50)); res.json(Array.from(jobs.values()).sort((a, b) => b.createdAt - a.createdAt).slice(0, 50));
}); });
// ─── ABS PROXY ───────────────────────────────────────────────────────────────
// Forwards API calls to Audiobookshelf to avoid CORS restrictions in the browser.
// The browser sends X-ABS-URL and X-ABS-Token headers; audio streaming uses
// direct <audio src> URLs and is unaffected by CORS.
app.all('/api/abs-proxy/*', async (req, res) => {
const absBase = req.headers['x-abs-url'];
const absToken = req.headers['x-abs-token'];
if (!absBase || !absToken) { res.status(400).json({ error: 'Missing X-ABS-URL or X-ABS-Token' }); return; }
// Strip the proxy prefix to get the ABS path (e.g. /api/me, /api/libraries/…)
const absPath = req.path.replace(/^\/api\/abs-proxy/, '');
const qs = Object.keys(req.query).length ? '?' + new URLSearchParams(req.query).toString() : '';
const url = `${absBase}${absPath}${qs}`;
const fwdHeaders = { 'Authorization': `Bearer ${absToken}`, 'Content-Type': 'application/json' };
const opts = { method: req.method, headers: fwdHeaders };
if (req.method !== 'GET' && req.method !== 'HEAD' && req.body && Object.keys(req.body).length) {
opts.body = JSON.stringify(req.body);
}
try {
const upstream = await fetch(url, opts);
const text = await upstream.text();
res.status(upstream.status)
.setHeader('Content-Type', upstream.headers.get('Content-Type') || 'application/json')
.send(text);
} catch (e) {
res.status(502).json({ error: `ABS proxy error: ${e.message}` });
}
});
// ─── START ──────────────────────────────────────────────────────────────────── // ─── START ────────────────────────────────────────────────────────────────────
app.listen(CONFIG.port, () => { app.listen(CONFIG.port, () => {
console.log(`\n🎵 NovaSonic Backend running on port ${CONFIG.port}`); console.log(`\n🎵 NovaSonic Backend running on port ${CONFIG.port}`);
+4 -6
View File
@@ -544,12 +544,10 @@
<label>Bibliothek</label> <label>Bibliothek</label>
<select id="abs-lib-select" onchange="absSetLibrary(this.value)" style="width:100%;padding:10px 12px;background:var(--bg2);border:1px solid var(--border2);border-radius:var(--radius-sm);color:var(--text);font-family:var(--font);font-size:14px;outline:none"></select> <select id="abs-lib-select" onchange="absSetLibrary(this.value)" style="width:100%;padding:10px 12px;background:var(--bg2);border:1px solid var(--border2);border-radius:var(--radius-sm);color:var(--text);font-family:var(--font);font-size:14px;outline:none"></select>
</div> </div>
<div class="settings-row" style="margin-top:8px;border-radius:9px"> <span id="abs-s-status" style="font-size:12px;color:var(--green);display:block;margin-bottom:8px;min-height:16px"></span>
<span id="abs-s-status" style="font-size:13px;color:var(--green)"></span> <div style="display:flex;gap:8px;margin-top:2px">
<div style="display:flex;gap:8px"> <button class="btn-outline" style="flex:1;padding:9px;font-size:13px;color:var(--danger);border-color:rgba(224,85,85,0.3)" onclick="absDisconnect()">Trennen</button>
<button class="btn-outline" style="padding:6px 12px;font-size:12px;color:var(--danger);border-color:rgba(224,85,85,0.3)" onclick="absDisconnect()">Trennen</button> <button class="btn-outline" id="abs-s-connect-btn" style="flex:1;padding:9px;font-size:13px;font-weight:700" onclick="absConnectSettings()">Verbinden</button>
<button class="btn-outline" id="abs-s-connect-btn" style="padding:6px 12px;font-size:12px" onclick="absConnectSettings()">Verbinden</button>
</div>
</div> </div>
</div> </div>
<div class="settings-section"> <div class="settings-section">
+20 -2
View File
@@ -2,12 +2,30 @@ import { S } from './state.js';
import { esc, toast } from './utils.js'; import { esc, toast } from './utils.js';
// ─── HTTP HELPERS ─── // ─── HTTP HELPERS ───
// API calls are routed through the NovaSonic backend proxy to avoid CORS restrictions.
// Audio streaming uses <audio src> directly (HTML media elements bypass CORS).
async function absRequest(method, path, body) { async function absRequest(method, path, body) {
const headers = { 'Content-Type': 'application/json' }; const backend = S.cfg.backendUrl;
let url, headers;
if (backend) {
// Proxy through NovaSonic backend — no CORS issue
url = `${backend}/api/abs-proxy${path}`;
headers = {
'Content-Type': 'application/json',
'X-ABS-URL': S.abs.url,
'X-ABS-Token': S.abs.token || '',
};
} else {
// Direct — requires CORS to be configured on the ABS server
url = `${S.abs.url}${path}`;
headers = { 'Content-Type': 'application/json' };
if (S.abs.token) headers['Authorization'] = `Bearer ${S.abs.token}`; if (S.abs.token) headers['Authorization'] = `Bearer ${S.abs.token}`;
}
const opts = { method, headers }; const opts = { method, headers };
if (body !== undefined) opts.body = JSON.stringify(body); if (body !== undefined) opts.body = JSON.stringify(body);
const res = await fetch(`${S.abs.url}${path}`, opts); const res = await fetch(url, opts);
if (!res.ok) { if (!res.ok) {
const txt = await res.text().catch(() => ''); const txt = await res.text().catch(() => '');
throw new Error(`ABS ${method} ${path}: HTTP ${res.status} ${txt.slice(0, 120)}`); throw new Error(`ABS ${method} ${path}: HTTP ${res.status} ${txt.slice(0, 120)}`);