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:
HoneyandClaude Sonnet 4.6 committed 2026-05-03 14:41:18 +02:00
1 parent c17b7ea11a
commit 7c6343e28e
4 files changed
+42 -25

No files matched your search

+25 -20
View File
@@ -20,13 +20,13 @@ const absPost = (p, b) => absRequest('POST', p, b);
const absPatch = (p, b) => absRequest('PATCH', p, b);
// ─── AUTH ───
export async function absLogin(url, user, pass) {
S.abs.url = url.replace(/\/$/, '');
const data = await absPost('/login', { username: user, password: pass });
if (!data.token) throw new Error('Kein Token in der Antwort');
S.abs.token = data.token;
S.abs.userId = data.user?.id || null;
return data;
export async function absVerifyApiKey(url, apiKey) {
S.abs.url = url.replace(/\/$/, '');
S.abs.token = apiKey;
// Verify by fetching current user — throws if token is invalid
const me = await absGet('/api/me');
S.abs.userId = me.id || null;
return me;
}
// ─── LIBRARIES ───
@@ -543,7 +543,6 @@ export function absInitFromStorage() {
const d = JSON.parse(localStorage.getItem('ns_abs') || 'null');
if (d?.url && d?.token) {
S.abs.url = d.url;
S.abs.user = d.user || '';
S.abs.token = d.token;
S.abs.userId = d.userId || null;
S.abs.currentLibraryId = d.libId || null;
@@ -553,38 +552,43 @@ export function absInitFromStorage() {
function absSaveToStorage() {
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,
}));
}
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() {
const url = document.getElementById('abs-s-url');
const user = document.getElementById('abs-s-user');
if (url) url.value = S.abs.url || '';
if (user) user.value = S.abs.user || '';
const urlEl = document.getElementById('abs-s-url');
if (urlEl) urlEl.value = S.abs.url || '';
// Never pre-fill the API key field for security — just show connection status
const statusEl = document.getElementById('abs-s-status');
if (statusEl) statusEl.textContent = S.abs.token ? '✅ Verbunden' : '';
}
export async function absConnectSettings() {
const url = document.getElementById('abs-s-url')?.value.trim().replace(/\/$/, '');
const user = document.getElementById('abs-s-user')?.value.trim();
const pass = document.getElementById('abs-s-pass')?.value;
if (!url || !user || !pass) { toast('❌ URL, Benutzer und Passwort ausfüllen'); return; }
const url = document.getElementById('abs-s-url')?.value.trim().replace(/\/$/, '');
const apiKey = document.getElementById('abs-s-apikey')?.value.trim();
if (!url || !apiKey) { toast('❌ URL und API Key ausfüllen'); return; }
const btn = document.getElementById('abs-s-connect-btn');
if (btn) { btn.disabled = true; btn.textContent = 'Verbinde…'; }
try {
await absLogin(url, user, pass);
await absVerifyApiKey(url, apiKey);
const libs = await absGetLibraries();
if (!S.abs.currentLibraryId) S.abs.currentLibraryId = libs[0]?.id || null;
absSaveToStorage();
_renderLibPicker(libs);
document.getElementById('nav-abs-section').style.display = '';
_showAbsNavItem(true);
toast('✅ Audiobookshelf verbunden');
const statusEl = document.getElementById('abs-s-status');
if (statusEl) statusEl.textContent = '✅ Verbunden';
} catch (e) {
S.abs.token = null;
toast(`❌ ABS Verbindung fehlgeschlagen: ${e.message}`);
} finally {
if (btn) { btn.disabled = false; btn.textContent = 'Verbinden'; }
@@ -617,9 +621,10 @@ export async function absLoadSettingsLibraries() {
}
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;
localStorage.removeItem('ns_abs');
_showAbsNavItem(false);
toast('Audiobookshelf getrennt');
const statusEl = document.getElementById('abs-s-status');
if (statusEl) statusEl.textContent = '';
+3 -1
View File
@@ -19,7 +19,7 @@ import {
} from './views.js';
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll } from './spotify.js';
import {
absInitFromStorage, absOpenSettings, absConnectSettings, absSetLibrary, absDisconnect,
absInitFromStorage, absOpenSettings, absConnectSettings, absSetLibrary, absDisconnect, absVerifyApiKey,
absPlayItem, absTogglePlay, absSeek, absSkipSecs, absNextChapter, absPrevChapter,
absSetSpeed, absSleepIn, absOpenPlayer, absClosePlayer, absToggleChapters,
absToggleSpeedPicker, absToggleSleepPicker, absSeekToChapter, absProgDown, absProgMove,
@@ -85,6 +85,8 @@ async function startApp() {
absInitFromStorage();
if (S.abs.token) {
document.getElementById('nav-abs-section').style.display = '';
const bnav = document.getElementById('bnav-audiobooks');
if (bnav) bnav.style.display = '';
absLoadSettingsLibraries();
}
if(_navLoad() && S._navCursor >= 0 && S._navStack[S._navCursor]) {
+1 -1
View File
@@ -48,7 +48,7 @@ export const S = {
// Audiobookshelf
abs: {
url: '', user: '', token: null, userId: null,
url: '', token: null, userId: null,
currentLibraryId: null,
},
absPlayer: {