diff --git a/documentation/backend/services/auth.md b/documentation/backend/services/auth.md index 5c35a22..779bcba 100644 --- a/documentation/backend/services/auth.md +++ b/documentation/backend/services/auth.md @@ -216,6 +216,8 @@ Automatically grants admin permissions based on OIDC claims (e.g., group members - **GET /api/auth/oidc/login** - Initiate OIDC flow, redirect to provider - **GET /api/auth/oidc/callback** - Handle OAuth callback, create/update user, return JWT - **GET /api/auth/providers** - List enabled auth providers for login page + - Returns: `backendMode`, `providers[]`, `registrationEnabled`, `hasLocalUsers`, `oidcProviderName`, `localLoginDisabled`, `automationEnabled` + - `automationEnabled`: true if Prowlarr/indexer configured (used for dynamic login page description) ### Configuration Keys ``` diff --git a/documentation/frontend/pages/login.md b/documentation/frontend/pages/login.md index b286cf3..668685f 100644 --- a/documentation/frontend/pages/login.md +++ b/documentation/frontend/pages/login.md @@ -2,7 +2,7 @@ **Status:** ✅ Implemented | Real floating book covers with professional animations -Stylized entry point with Plex OAuth integration, animated floating popular audiobook covers, and prominent "Login with Plex" CTA. +Stylized entry point with Plex/Audiobookshelf authentication, animated floating popular audiobook covers, and dynamic description based on backend configuration. ## Design @@ -13,6 +13,7 @@ Stylized entry point with Plex OAuth integration, animated floating popular audi - Multi-layer depth effect with z-index layering (0-20) - Dark theme optimized with glassmorphism card - Professional streaming service aesthetic +- **Dynamic description** based on backend mode (Plex/Audiobookshelf) and automation status ## Authentication Flow @@ -53,6 +54,18 @@ Stylized entry point with Plex OAuth integration, animated floating popular audi - Seed multipliers (7, 13, 17, 23, 29, 31) prevent pattern repetition - Math.sin() based pseudo-random for deterministic results +## Dynamic Description + +Description text adapts to backend configuration: + +**Plex + Automation Enabled:** "Request audiobooks and they'll automatically download and appear in your Plex library" +**Plex + No Automation:** "Request audiobooks for your Plex library" +**Audiobookshelf + Automation:** "Request audiobooks and they'll automatically download and appear in your Audiobookshelf library" +**Audiobookshelf + No Automation:** "Request audiobooks for your Audiobookshelf library" +**Loading State:** "Your Personal Audiobook Library Manager" + +Automation is detected by checking for configured indexer (Prowlarr) via `/api/auth/providers` endpoint. + ## State ```typescript @@ -65,6 +78,15 @@ interface LoginPageState { showAdminLogin: boolean; adminUsername: string; adminPassword: string; + authProviders: { + backendMode: string; + providers: string[]; + registrationEnabled: boolean; + hasLocalUsers: boolean; + oidcProviderName: string | null; + localLoginDisabled: boolean; + automationEnabled: boolean; + } | null; } interface BookCover { diff --git a/documentation/phase3/qbittorrent.md b/documentation/phase3/qbittorrent.md index 96d96a5..e9e1403 100644 --- a/documentation/phase3/qbittorrent.md +++ b/documentation/phase3/qbittorrent.md @@ -181,6 +181,13 @@ type TorrentState = 'downloading' | 'uploading' | 'stalledDL' | - Headers set to qBittorrent base URL (e.g., `https://seedbox.example.com:443/qbittorrent`) - Applied to both `login()` and `testConnectionWithCredentials()` methods - Works with all qBittorrent versions and configurations + - Enhanced debug logging for troubleshooting authentication issues (enable with `LOG_LEVEL=debug`) +**13. Nginx/Apache reverse proxy HTTP Basic Auth** - Many seedboxes use nginx or Apache reverse proxy with HTTP Basic Authentication in front of qBittorrent. This causes HTTP 401 errors with `www-authenticate: Basic` header. Browsers handle this by prompting for credentials and sending `Authorization: Basic` header. Fixed by: + - Adding HTTP Basic Auth to all axios requests using `auth` parameter + - Same credentials used for both Basic Auth (nginx/Apache) and qBittorrent Web UI authentication + - Applied to axios client instance and all standalone requests + - Works transparently with or without reverse proxy + - Compatible with popular seedbox providers (seedit4.me, etc.) ## Tech Stack diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 76ce013..a4c16e3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -355,9 +355,10 @@ model ScheduledJob { model BookDateConfig { id String @id @default(uuid()) - provider String // 'openai' | 'claude' + provider String // 'openai' | 'claude' | 'custom' apiKey String @map("api_key") @db.Text // Encrypted at rest (AES-256) model String // e.g., 'gpt-4o', 'claude-sonnet-4-5-20250929' + baseUrl String? @map("base_url") @db.Text // Base URL for custom provider (OpenAI-compatible endpoints) libraryScope String? @map("library_scope") // DEPRECATED: Now per-user (User.bookDateLibraryScope) customPrompt String? @map("custom_prompt") @db.Text // DEPRECATED: Now per-user (User.bookDateCustomPrompt) isVerified Boolean @default(false) @map("is_verified") diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx index cb6b6fa..3d116d5 100644 --- a/src/app/admin/settings/page.tsx +++ b/src/app/admin/settings/page.tsx @@ -150,6 +150,7 @@ export default function AdminSettings() { const [bookdateProvider, setBookdateProvider] = useState('openai'); const [bookdateApiKey, setBookdateApiKey] = useState(''); const [bookdateModel, setBookdateModel] = useState(''); + const [bookdateBaseUrl, setBookdateBaseUrl] = useState(''); const [bookdateEnabled, setBookdateEnabled] = useState(true); const [bookdateConfigured, setBookdateConfigured] = useState(false); const [bookdateModels, setBookdateModels] = useState<{ id: string; name: string }[]>([]); @@ -341,6 +342,7 @@ export default function AdminSettings() { if (data.config) { setBookdateProvider(data.config.provider || 'openai'); setBookdateModel(data.config.model || ''); + setBookdateBaseUrl(data.config.baseUrl || ''); setBookdateEnabled(data.config.isEnabled !== false); // Default to true setBookdateConfigured(data.config.isVerified || false); } @@ -352,10 +354,18 @@ export default function AdminSettings() { const handleTestBookdateConnection = async () => { const hasApiKey = bookdateApiKey.trim().length > 0; - // Allow testing with saved API key if already configured - if (!hasApiKey && !bookdateConfigured) { - setMessage({ type: 'error', text: 'Please enter an API key' }); - return; + // Validation + if (bookdateProvider === 'custom') { + if (!bookdateBaseUrl.trim()) { + setMessage({ type: 'error', text: 'Please enter a base URL for custom provider' }); + return; + } + } else { + // Allow testing with saved API key if already configured + if (!hasApiKey && !bookdateConfigured) { + setMessage({ type: 'error', text: 'Please enter an API key' }); + return; + } } setTestingBookdate(true); @@ -369,10 +379,15 @@ export default function AdminSettings() { // Include API key if user entered a new one, otherwise use saved key if (hasApiKey) { payload.apiKey = bookdateApiKey; - } else { + } else if (bookdateProvider !== 'custom') { payload.useSavedKey = true; } + // Include baseUrl for custom provider + if (bookdateProvider === 'custom') { + payload.baseUrl = bookdateBaseUrl; + } + const response = await fetchWithAuth('/api/bookdate/test-connection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -406,17 +421,26 @@ export default function AdminSettings() { return; } - // Only require API key if not already configured OR if user entered one - const hasApiKey = bookdateApiKey.trim().length > 0; - if (!bookdateConfigured && !hasApiKey) { - setMessage({ type: 'error', text: 'Please enter an API key for initial setup' }); - return; + // Validate: baseUrl required for custom provider + if (bookdateProvider === 'custom') { + if (!bookdateBaseUrl.trim()) { + setMessage({ type: 'error', text: 'Please enter a base URL for custom provider' }); + return; + } + } else { + // Only require API key if not already configured OR if user entered one + const hasApiKey = bookdateApiKey.trim().length > 0; + if (!bookdateConfigured && !hasApiKey) { + setMessage({ type: 'error', text: 'Please enter an API key for initial setup' }); + return; + } } setSaving(true); setMessage(null); try { + const hasApiKey = bookdateApiKey.trim().length > 0; const payload: any = { provider: bookdateProvider, model: bookdateModel, @@ -428,6 +452,11 @@ export default function AdminSettings() { payload.apiKey = bookdateApiKey; } + // Include baseUrl for custom provider + if (bookdateProvider === 'custom') { + payload.baseUrl = bookdateBaseUrl; + } + const response = await fetchWithAuth('/api/bookdate/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -2325,18 +2354,45 @@ export default function AdminSettings() { onChange={(e) => { setBookdateProvider(e.target.value); setBookdateModels([]); + setBookdateBaseUrl(''); }} className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500" > + + {/* Base URL Input - Show for Custom Provider */} + {bookdateProvider === 'custom' && ( +
+ + { + setBookdateBaseUrl(e.target.value); + setBookdateModels([]); + }} + placeholder="http://localhost:11434/v1" + /> +

+ Examples: +
• Ollama: http://localhost:11434/v1 +
• LM Studio: http://localhost:1234/v1 +
• vLLM: http://localhost:8000/v1 +

+
+ )} + {/* API Key */}

- The API key is stored securely and encrypted. Leave blank to keep existing key. + {bookdateProvider === 'custom' + ? 'Optional: Leave blank if your endpoint does not require authentication (e.g., Ollama, LM Studio)' + : 'The API key is stored securely and encrypted. Leave blank to keep existing key.'}

@@ -2360,7 +2420,11 @@ export default function AdminSettings() {