diff --git a/src/app/api/admin/settings/download-clients/[id]/route.ts b/src/app/api/admin/settings/download-clients/[id]/route.ts index 8b90d03..f3ab796 100644 --- a/src/app/api/admin/settings/download-clients/[id]/route.ts +++ b/src/app/api/admin/settings/download-clients/[id]/route.ts @@ -77,12 +77,16 @@ export async function PUT( } } - // Test connection if credentials/URL changed + // Test connection if credentials/URL changed (skip if disabling client) + const isDisabling = enabled === false; if ( - url !== undefined || - username !== undefined || - (password && password !== '********') || - disableSSLVerify !== undefined + !isDisabling && + ( + url !== undefined || + username !== undefined || + (password && password !== '********') || + disableSSLVerify !== undefined + ) ) { const testResult = await manager.testConnection(updatedClient); if (!testResult.success) { diff --git a/src/app/api/admin/settings/download-clients/route.ts b/src/app/api/admin/settings/download-clients/route.ts index cebc747..62760db 100644 --- a/src/app/api/admin/settings/download-clients/route.ts +++ b/src/app/api/admin/settings/download-clients/route.ts @@ -72,17 +72,18 @@ export async function POST(request: NextRequest) { } // Validate required fields - if (!name || !url || !password) { + // Name and URL always required; password/API key only required for SABnzbd + // qBittorrent supports IP whitelist auth (no credentials needed) + if (!name || !url) { return NextResponse.json( - { error: 'Name, URL, and password/API key are required' }, + { error: 'Name and URL are required' }, { status: 400 } ); } - // qBittorrent requires username - if (type === 'qbittorrent' && !username) { + if (type === 'sabnzbd' && !password) { return NextResponse.json( - { error: 'Username is required for qBittorrent' }, + { error: 'API key is required for SABnzbd' }, { status: 400 } ); } @@ -111,14 +112,15 @@ export async function POST(request: NextRequest) { } // Create new client config + // qBittorrent credentials are optional (supports IP whitelist auth) const newClient: DownloadClientConfig = { id: randomUUID(), type, name, enabled: true, url, - username: username || undefined, - password, + username: username || '', + password: password || '', disableSSLVerify: disableSSLVerify || false, remotePathMappingEnabled: remotePathMappingEnabled || false, remotePath: remotePath || undefined, diff --git a/src/app/api/admin/settings/download-clients/test/route.ts b/src/app/api/admin/settings/download-clients/test/route.ts index fa0a701..a4e5ef6 100644 --- a/src/app/api/admin/settings/download-clients/test/route.ts +++ b/src/app/api/admin/settings/download-clients/test/route.ts @@ -66,29 +66,32 @@ export async function POST(request: NextRequest) { } // Validate required fields - if (!url || !effectivePassword) { + // URL is always required; password/API key only required for SABnzbd + // qBittorrent supports IP whitelist auth (no credentials needed) + if (!url) { return NextResponse.json( - { error: 'URL and password/API key are required' }, + { error: 'URL is required' }, { status: 400 } ); } - if (type === 'qbittorrent' && !effectiveUsername) { + if (type === 'sabnzbd' && !effectivePassword) { return NextResponse.json( - { error: 'Username is required for qBittorrent' }, + { error: 'API key is required for SABnzbd' }, { status: 400 } ); } // Create temporary client config for testing + // qBittorrent credentials are optional (supports IP whitelist auth) const testConfig: DownloadClientConfig = { id: 'test', type, name: 'Test Client', enabled: true, url, - username: effectiveUsername || undefined, - password: effectivePassword, + username: effectiveUsername || '', + password: effectivePassword || '', disableSSLVerify: disableSSLVerify || false, remotePathMappingEnabled: remotePathMappingEnabled || false, remotePath: remotePath || undefined, diff --git a/src/app/api/setup/complete/route.ts b/src/app/api/setup/complete/route.ts index 9ebd744..ae26e2d 100644 --- a/src/app/api/setup/complete/route.ts +++ b/src/app/api/setup/complete/route.ts @@ -107,16 +107,18 @@ export async function POST(request: NextRequest) { } // Validate each client has required fields + // qBittorrent credentials are optional (supports IP whitelist auth) + // SABnzbd always requires API key for (const client of clients) { - if (!client.url || !client.password) { + if (!client.url) { return NextResponse.json( - { success: false, error: 'Download client URL and password/API key are required' }, + { success: false, error: 'Download client URL is required' }, { status: 400 } ); } - if (client.type === 'qbittorrent' && !client.username) { + if (client.type === 'sabnzbd' && !client.password) { return NextResponse.json( - { success: false, error: 'qBittorrent username is required' }, + { success: false, error: 'SABnzbd API key is required' }, { status: 400 } ); } diff --git a/src/app/api/setup/test-download-client/route.ts b/src/app/api/setup/test-download-client/route.ts index 99336a5..9ddc5e1 100644 --- a/src/app/api/setup/test-download-client/route.ts +++ b/src/app/api/setup/test-download-client/route.ts @@ -29,19 +29,13 @@ export async function POST(request: NextRequest) { } // Validate required fields per client type + // qBittorrent credentials are optional (supports IP whitelist auth) if (type === 'qbittorrent') { - if (!username || !password) { - return NextResponse.json( - { success: false, error: 'Username and password are required for qBittorrent' }, - { status: 400 } - ); - } - - // Test qBittorrent connection + // Test qBittorrent connection (empty credentials work with IP whitelist) const version = await QBittorrentService.testConnectionWithCredentials( url, - username, - password, + username || '', + password || '', disableSSLVerify || false ); diff --git a/src/components/admin/download-clients/DownloadClientModal.tsx b/src/components/admin/download-clients/DownloadClientModal.tsx index 88b6749..b0f3c14 100644 --- a/src/components/admin/download-clients/DownloadClientModal.tsx +++ b/src/components/admin/download-clients/DownloadClientModal.tsx @@ -108,12 +108,9 @@ export function DownloadClientModal({ newErrors.url = 'URL is required'; } - if (type === 'qbittorrent' && !username.trim()) { - newErrors.username = 'Username is required for qBittorrent'; - } - - if (!password.trim() || (mode === 'add' && password === '********')) { - newErrors.password = type === 'qbittorrent' ? 'Password is required' : 'API key is required'; + // SABnzbd always requires API key; qBittorrent credentials are optional (supports IP whitelist auth) + if (type === 'sabnzbd' && (!password.trim() || (mode === 'add' && password === '********'))) { + newErrors.password = 'API key is required'; } if (remotePathMappingEnabled) { @@ -196,7 +193,8 @@ export function DownloadClientModal({ return; } - if (!testResult?.success) { + // Skip connection test requirement when disabling the client + if (!testResult?.success && enabled) { setErrors({ ...errors, test: 'Please test the connection before saving' }); return; } @@ -439,7 +437,7 @@ export function DownloadClientModal({ diff --git a/src/lib/services/download-client-manager.service.ts b/src/lib/services/download-client-manager.service.ts index c82190c..361dd64 100644 --- a/src/lib/services/download-client-manager.service.ts +++ b/src/lib/services/download-client-manager.service.ts @@ -184,7 +184,7 @@ export class DownloadClientManager { return new QBittorrentService( config.url, config.username || '', - config.password, + config.password || '', // Optional for IP whitelist auth '/downloads', // defaultSavePath config.category || 'readmeabook', // defaultCategory config.disableSSLVerify, diff --git a/tests/api/setup-tests.routes.test.ts b/tests/api/setup-tests.routes.test.ts index fcd6b96..7dcd4dc 100644 --- a/tests/api/setup-tests.routes.test.ts +++ b/tests/api/setup-tests.routes.test.ts @@ -163,15 +163,15 @@ describe('Setup test routes', () => { expect(payload.error).toMatch(/Invalid client type/); }); - it('rejects missing qBittorrent credentials', async () => { + it('rejects missing SABnzbd API key', async () => { const { POST } = await import('@/app/api/setup/test-download-client/route'); const response = await POST({ - json: vi.fn().mockResolvedValue({ type: 'qbittorrent', url: 'http://qbt' }), + json: vi.fn().mockResolvedValue({ type: 'sabnzbd', url: 'http://sab' }), } as any); const payload = await response.json(); expect(response.status).toBe(400); - expect(payload.error).toMatch(/Username and password/); + expect(payload.error).toMatch(/API key/); }); it('tests SABnzbd connection', async () => {