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:
1 parent
7c6343e28e
commit
d70f649b90
3 files changed
+56
-9
No files matched your search
@@ -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));
|
||||
});
|
||||
|
||||
// ─── 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 ────────────────────────────────────────────────────────────────────
|
||||
app.listen(CONFIG.port, () => {
|
||||
console.log(`\n🎵 NovaSonic Backend running on port ${CONFIG.port}`);
|
||||
|
||||
+4
-6
@@ -544,12 +544,10 @@
|
||||
<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>
|
||||
</div>
|
||||
<div class="settings-row" style="margin-top:8px;border-radius:9px">
|
||||
<span id="abs-s-status" style="font-size:13px;color:var(--green)"></span>
|
||||
<div style="display:flex;gap:8px">
|
||||
<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="padding:6px 12px;font-size:12px" onclick="absConnectSettings()">Verbinden</button>
|
||||
</div>
|
||||
<span id="abs-s-status" style="font-size:12px;color:var(--green);display:block;margin-bottom:8px;min-height:16px"></span>
|
||||
<div style="display:flex;gap:8px;margin-top:2px">
|
||||
<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" id="abs-s-connect-btn" style="flex:1;padding:9px;font-size:13px;font-weight:700" onclick="absConnectSettings()">Verbinden</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
|
||||
+21
-3
@@ -2,12 +2,30 @@ import { S } from './state.js';
|
||||
import { esc, toast } from './utils.js';
|
||||
|
||||
// ─── 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) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (S.abs.token) headers['Authorization'] = `Bearer ${S.abs.token}`;
|
||||
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}`;
|
||||
}
|
||||
|
||||
const opts = { method, headers };
|
||||
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) {
|
||||
const txt = await res.text().catch(() => '');
|
||||
throw new Error(`ABS ${method} ${path}: HTTP ${res.status} ${txt.slice(0, 120)}`);
|
||||
|
||||
Reference in new issue
Block a user