fix: ABS API key auth + mobile bottom-nav Hörbücher button
- Replace username/password login with API key (Bearer token) auth - New absVerifyApiKey() verifies via GET /api/me instead of POST /login - Settings shows single API Key field with hint (Profil → API Token in ABS) - API key never pre-filled in settings for security - Add "Bücher" button to mobile bottom-nav (bnav-library), hidden until connected - _showAbsNavItem() helper keeps sidebar + bnav in sync on connect/disconnect Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
c17b7ea11a
commit
7c6343e28e
4 files changed
+42
-25
No files matched your search
+13
-3
@@ -531,9 +531,15 @@
|
|||||||
<span style="width:24px;height:24px;border-radius:6px;background:var(--green-dim);display:inline-flex;align-items:center;justify-content:center;font-size:12px">📚</span>
|
<span style="width:24px;height:24px;border-radius:6px;background:var(--green-dim);display:inline-flex;align-items:center;justify-content:center;font-size:12px">📚</span>
|
||||||
Audiobookshelf
|
Audiobookshelf
|
||||||
</h3>
|
</h3>
|
||||||
<div class="field"><label>Server URL</label><input id="abs-s-url" type="url" placeholder="https://books.example.com"></div>
|
<div class="field">
|
||||||
<div class="field"><label>Benutzername</label><input id="abs-s-user" type="text" placeholder="admin"></div>
|
<label>Server URL</label>
|
||||||
<div class="field"><label>Passwort</label><input id="abs-s-pass" type="password" placeholder="••••••••"></div>
|
<input id="abs-s-url" type="url" placeholder="https://books.example.com">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>API Key</label>
|
||||||
|
<input id="abs-s-apikey" type="password" placeholder="Aus ABS: Profil → API Token">
|
||||||
|
<div class="hint">Einstellungen → Profil → API Token in Audiobookshelf</div>
|
||||||
|
</div>
|
||||||
<div id="abs-lib-picker" style="display:none" class="field">
|
<div id="abs-lib-picker" style="display:none" class="field">
|
||||||
<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>
|
||||||
@@ -895,6 +901,10 @@
|
|||||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||||
Favoriten
|
Favoriten
|
||||||
</button>
|
</button>
|
||||||
|
<button class="bnav-item" id="bnav-audiobooks" onclick="navigate('audiobooks')" data-page="audiobooks" style="display:none">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
||||||
|
Bücher
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="bnav-pages" id="bnav-discover" style="display:none">
|
<div class="bnav-pages" id="bnav-discover" style="display:none">
|
||||||
<button class="bnav-item" onclick="navigate('discover')" data-page="discover">
|
<button class="bnav-item" onclick="navigate('discover')" data-page="discover">
|
||||||
|
|||||||
+25
-20
@@ -20,13 +20,13 @@ const absPost = (p, b) => absRequest('POST', p, b);
|
|||||||
const absPatch = (p, b) => absRequest('PATCH', p, b);
|
const absPatch = (p, b) => absRequest('PATCH', p, b);
|
||||||
|
|
||||||
// ─── AUTH ───
|
// ─── AUTH ───
|
||||||
export async function absLogin(url, user, pass) {
|
export async function absVerifyApiKey(url, apiKey) {
|
||||||
S.abs.url = url.replace(/\/$/, '');
|
S.abs.url = url.replace(/\/$/, '');
|
||||||
const data = await absPost('/login', { username: user, password: pass });
|
S.abs.token = apiKey;
|
||||||
if (!data.token) throw new Error('Kein Token in der Antwort');
|
// Verify by fetching current user — throws if token is invalid
|
||||||
S.abs.token = data.token;
|
const me = await absGet('/api/me');
|
||||||
S.abs.userId = data.user?.id || null;
|
S.abs.userId = me.id || null;
|
||||||
return data;
|
return me;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── LIBRARIES ───
|
// ─── LIBRARIES ───
|
||||||
@@ -543,7 +543,6 @@ export function absInitFromStorage() {
|
|||||||
const d = JSON.parse(localStorage.getItem('ns_abs') || 'null');
|
const d = JSON.parse(localStorage.getItem('ns_abs') || 'null');
|
||||||
if (d?.url && d?.token) {
|
if (d?.url && d?.token) {
|
||||||
S.abs.url = d.url;
|
S.abs.url = d.url;
|
||||||
S.abs.user = d.user || '';
|
|
||||||
S.abs.token = d.token;
|
S.abs.token = d.token;
|
||||||
S.abs.userId = d.userId || null;
|
S.abs.userId = d.userId || null;
|
||||||
S.abs.currentLibraryId = d.libId || null;
|
S.abs.currentLibraryId = d.libId || null;
|
||||||
@@ -553,38 +552,43 @@ export function absInitFromStorage() {
|
|||||||
|
|
||||||
function absSaveToStorage() {
|
function absSaveToStorage() {
|
||||||
localStorage.setItem('ns_abs', JSON.stringify({
|
localStorage.setItem('ns_abs', JSON.stringify({
|
||||||
url: S.abs.url, user: S.abs.user, token: S.abs.token,
|
url: S.abs.url, token: S.abs.token,
|
||||||
userId: S.abs.userId, libId: S.abs.currentLibraryId,
|
userId: S.abs.userId, libId: S.abs.currentLibraryId,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _showAbsNavItem(show) {
|
||||||
|
document.getElementById('nav-abs-section')?.style && (document.getElementById('nav-abs-section').style.display = show ? '' : 'none');
|
||||||
|
const bnav = document.getElementById('bnav-audiobooks');
|
||||||
|
if (bnav) bnav.style.display = show ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
export function absOpenSettings() {
|
export function absOpenSettings() {
|
||||||
const url = document.getElementById('abs-s-url');
|
const urlEl = document.getElementById('abs-s-url');
|
||||||
const user = document.getElementById('abs-s-user');
|
if (urlEl) urlEl.value = S.abs.url || '';
|
||||||
if (url) url.value = S.abs.url || '';
|
// Never pre-fill the API key field for security — just show connection status
|
||||||
if (user) user.value = S.abs.user || '';
|
|
||||||
const statusEl = document.getElementById('abs-s-status');
|
const statusEl = document.getElementById('abs-s-status');
|
||||||
if (statusEl) statusEl.textContent = S.abs.token ? '✅ Verbunden' : '';
|
if (statusEl) statusEl.textContent = S.abs.token ? '✅ Verbunden' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function absConnectSettings() {
|
export async function absConnectSettings() {
|
||||||
const url = document.getElementById('abs-s-url')?.value.trim().replace(/\/$/, '');
|
const url = document.getElementById('abs-s-url')?.value.trim().replace(/\/$/, '');
|
||||||
const user = document.getElementById('abs-s-user')?.value.trim();
|
const apiKey = document.getElementById('abs-s-apikey')?.value.trim();
|
||||||
const pass = document.getElementById('abs-s-pass')?.value;
|
if (!url || !apiKey) { toast('❌ URL und API Key ausfüllen'); return; }
|
||||||
if (!url || !user || !pass) { toast('❌ URL, Benutzer und Passwort ausfüllen'); return; }
|
|
||||||
const btn = document.getElementById('abs-s-connect-btn');
|
const btn = document.getElementById('abs-s-connect-btn');
|
||||||
if (btn) { btn.disabled = true; btn.textContent = 'Verbinde…'; }
|
if (btn) { btn.disabled = true; btn.textContent = 'Verbinde…'; }
|
||||||
try {
|
try {
|
||||||
await absLogin(url, user, pass);
|
await absVerifyApiKey(url, apiKey);
|
||||||
const libs = await absGetLibraries();
|
const libs = await absGetLibraries();
|
||||||
if (!S.abs.currentLibraryId) S.abs.currentLibraryId = libs[0]?.id || null;
|
if (!S.abs.currentLibraryId) S.abs.currentLibraryId = libs[0]?.id || null;
|
||||||
absSaveToStorage();
|
absSaveToStorage();
|
||||||
_renderLibPicker(libs);
|
_renderLibPicker(libs);
|
||||||
document.getElementById('nav-abs-section').style.display = '';
|
_showAbsNavItem(true);
|
||||||
toast('✅ Audiobookshelf verbunden');
|
toast('✅ Audiobookshelf verbunden');
|
||||||
const statusEl = document.getElementById('abs-s-status');
|
const statusEl = document.getElementById('abs-s-status');
|
||||||
if (statusEl) statusEl.textContent = '✅ Verbunden';
|
if (statusEl) statusEl.textContent = '✅ Verbunden';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
S.abs.token = null;
|
||||||
toast(`❌ ABS Verbindung fehlgeschlagen: ${e.message}`);
|
toast(`❌ ABS Verbindung fehlgeschlagen: ${e.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
if (btn) { btn.disabled = false; btn.textContent = 'Verbinden'; }
|
if (btn) { btn.disabled = false; btn.textContent = 'Verbinden'; }
|
||||||
@@ -617,9 +621,10 @@ export async function absLoadSettingsLibraries() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function absDisconnect() {
|
export function absDisconnect() {
|
||||||
S.abs.url = ''; S.abs.user = ''; S.abs.token = null;
|
S.abs.url = ''; S.abs.token = null;
|
||||||
S.abs.userId = null; S.abs.currentLibraryId = null;
|
S.abs.userId = null; S.abs.currentLibraryId = null;
|
||||||
localStorage.removeItem('ns_abs');
|
localStorage.removeItem('ns_abs');
|
||||||
|
_showAbsNavItem(false);
|
||||||
toast('Audiobookshelf getrennt');
|
toast('Audiobookshelf getrennt');
|
||||||
const statusEl = document.getElementById('abs-s-status');
|
const statusEl = document.getElementById('abs-s-status');
|
||||||
if (statusEl) statusEl.textContent = '';
|
if (statusEl) statusEl.textContent = '';
|
||||||
|
|||||||
+3
-1
@@ -19,7 +19,7 @@ import {
|
|||||||
} from './views.js';
|
} from './views.js';
|
||||||
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll } from './spotify.js';
|
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll } from './spotify.js';
|
||||||
import {
|
import {
|
||||||
absInitFromStorage, absOpenSettings, absConnectSettings, absSetLibrary, absDisconnect,
|
absInitFromStorage, absOpenSettings, absConnectSettings, absSetLibrary, absDisconnect, absVerifyApiKey,
|
||||||
absPlayItem, absTogglePlay, absSeek, absSkipSecs, absNextChapter, absPrevChapter,
|
absPlayItem, absTogglePlay, absSeek, absSkipSecs, absNextChapter, absPrevChapter,
|
||||||
absSetSpeed, absSleepIn, absOpenPlayer, absClosePlayer, absToggleChapters,
|
absSetSpeed, absSleepIn, absOpenPlayer, absClosePlayer, absToggleChapters,
|
||||||
absToggleSpeedPicker, absToggleSleepPicker, absSeekToChapter, absProgDown, absProgMove,
|
absToggleSpeedPicker, absToggleSleepPicker, absSeekToChapter, absProgDown, absProgMove,
|
||||||
@@ -85,6 +85,8 @@ async function startApp() {
|
|||||||
absInitFromStorage();
|
absInitFromStorage();
|
||||||
if (S.abs.token) {
|
if (S.abs.token) {
|
||||||
document.getElementById('nav-abs-section').style.display = '';
|
document.getElementById('nav-abs-section').style.display = '';
|
||||||
|
const bnav = document.getElementById('bnav-audiobooks');
|
||||||
|
if (bnav) bnav.style.display = '';
|
||||||
absLoadSettingsLibraries();
|
absLoadSettingsLibraries();
|
||||||
}
|
}
|
||||||
if(_navLoad() && S._navCursor >= 0 && S._navStack[S._navCursor]) {
|
if(_navLoad() && S._navCursor >= 0 && S._navStack[S._navCursor]) {
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ export const S = {
|
|||||||
|
|
||||||
// Audiobookshelf
|
// Audiobookshelf
|
||||||
abs: {
|
abs: {
|
||||||
url: '', user: '', token: null, userId: null,
|
url: '', token: null, userId: null,
|
||||||
currentLibraryId: null,
|
currentLibraryId: null,
|
||||||
},
|
},
|
||||||
absPlayer: {
|
absPlayer: {
|
||||||
|
|||||||
Reference in new issue
Block a user