mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 21:00:09 +00:00
Optional qBittorrent creds; require SABnzbd key
Allow qBittorrent to be configured without credentials (supports IP whitelist) and require an API key for SABnzbd. Skip connection testing when disabling a client. Updates include: validation changes in admin and setup API routes, test-download-client flows, DownloadClientModal UI validation and save/test logic, and DownloadClientManager to pass empty strings for optional credentials. Tests updated to reflect SABnzbd API key requirement.
This commit is contained in:
@@ -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 (
|
if (
|
||||||
|
!isDisabling &&
|
||||||
|
(
|
||||||
url !== undefined ||
|
url !== undefined ||
|
||||||
username !== undefined ||
|
username !== undefined ||
|
||||||
(password && password !== '********') ||
|
(password && password !== '********') ||
|
||||||
disableSSLVerify !== undefined
|
disableSSLVerify !== undefined
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
const testResult = await manager.testConnection(updatedClient);
|
const testResult = await manager.testConnection(updatedClient);
|
||||||
if (!testResult.success) {
|
if (!testResult.success) {
|
||||||
|
|||||||
@@ -72,17 +72,18 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// 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(
|
return NextResponse.json(
|
||||||
{ error: 'Name, URL, and password/API key are required' },
|
{ error: 'Name and URL are required' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// qBittorrent requires username
|
if (type === 'sabnzbd' && !password) {
|
||||||
if (type === 'qbittorrent' && !username) {
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Username is required for qBittorrent' },
|
{ error: 'API key is required for SABnzbd' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -111,14 +112,15 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new client config
|
// Create new client config
|
||||||
|
// qBittorrent credentials are optional (supports IP whitelist auth)
|
||||||
const newClient: DownloadClientConfig = {
|
const newClient: DownloadClientConfig = {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
url,
|
url,
|
||||||
username: username || undefined,
|
username: username || '',
|
||||||
password,
|
password: password || '',
|
||||||
disableSSLVerify: disableSSLVerify || false,
|
disableSSLVerify: disableSSLVerify || false,
|
||||||
remotePathMappingEnabled: remotePathMappingEnabled || false,
|
remotePathMappingEnabled: remotePathMappingEnabled || false,
|
||||||
remotePath: remotePath || undefined,
|
remotePath: remotePath || undefined,
|
||||||
|
|||||||
@@ -66,29 +66,32 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// 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(
|
return NextResponse.json(
|
||||||
{ error: 'URL and password/API key are required' },
|
{ error: 'URL is required' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'qbittorrent' && !effectiveUsername) {
|
if (type === 'sabnzbd' && !effectivePassword) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Username is required for qBittorrent' },
|
{ error: 'API key is required for SABnzbd' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create temporary client config for testing
|
// Create temporary client config for testing
|
||||||
|
// qBittorrent credentials are optional (supports IP whitelist auth)
|
||||||
const testConfig: DownloadClientConfig = {
|
const testConfig: DownloadClientConfig = {
|
||||||
id: 'test',
|
id: 'test',
|
||||||
type,
|
type,
|
||||||
name: 'Test Client',
|
name: 'Test Client',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
url,
|
url,
|
||||||
username: effectiveUsername || undefined,
|
username: effectiveUsername || '',
|
||||||
password: effectivePassword,
|
password: effectivePassword || '',
|
||||||
disableSSLVerify: disableSSLVerify || false,
|
disableSSLVerify: disableSSLVerify || false,
|
||||||
remotePathMappingEnabled: remotePathMappingEnabled || false,
|
remotePathMappingEnabled: remotePathMappingEnabled || false,
|
||||||
remotePath: remotePath || undefined,
|
remotePath: remotePath || undefined,
|
||||||
|
|||||||
@@ -107,16 +107,18 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate each client has required fields
|
// Validate each client has required fields
|
||||||
|
// qBittorrent credentials are optional (supports IP whitelist auth)
|
||||||
|
// SABnzbd always requires API key
|
||||||
for (const client of clients) {
|
for (const client of clients) {
|
||||||
if (!client.url || !client.password) {
|
if (!client.url) {
|
||||||
return NextResponse.json(
|
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 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (client.type === 'qbittorrent' && !client.username) {
|
if (client.type === 'sabnzbd' && !client.password) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: 'qBittorrent username is required' },
|
{ success: false, error: 'SABnzbd API key is required' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,19 +29,13 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields per client type
|
// Validate required fields per client type
|
||||||
|
// qBittorrent credentials are optional (supports IP whitelist auth)
|
||||||
if (type === 'qbittorrent') {
|
if (type === 'qbittorrent') {
|
||||||
if (!username || !password) {
|
// Test qBittorrent connection (empty credentials work with IP whitelist)
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Username and password are required for qBittorrent' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test qBittorrent connection
|
|
||||||
const version = await QBittorrentService.testConnectionWithCredentials(
|
const version = await QBittorrentService.testConnectionWithCredentials(
|
||||||
url,
|
url,
|
||||||
username,
|
username || '',
|
||||||
password,
|
password || '',
|
||||||
disableSSLVerify || false
|
disableSSLVerify || false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -108,12 +108,9 @@ export function DownloadClientModal({
|
|||||||
newErrors.url = 'URL is required';
|
newErrors.url = 'URL is required';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'qbittorrent' && !username.trim()) {
|
// SABnzbd always requires API key; qBittorrent credentials are optional (supports IP whitelist auth)
|
||||||
newErrors.username = 'Username is required for qBittorrent';
|
if (type === 'sabnzbd' && (!password.trim() || (mode === 'add' && password === '********'))) {
|
||||||
}
|
newErrors.password = 'API key is required';
|
||||||
|
|
||||||
if (!password.trim() || (mode === 'add' && password === '********')) {
|
|
||||||
newErrors.password = type === 'qbittorrent' ? 'Password is required' : 'API key is required';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remotePathMappingEnabled) {
|
if (remotePathMappingEnabled) {
|
||||||
@@ -196,7 +193,8 @@ export function DownloadClientModal({
|
|||||||
return;
|
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' });
|
setErrors({ ...errors, test: 'Please test the connection before saving' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -439,7 +437,7 @@ export function DownloadClientModal({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving || !testResult?.success}
|
disabled={saving || (!testResult?.success && enabled)}
|
||||||
>
|
>
|
||||||
{saving ? 'Saving...' : mode === 'add' ? 'Add Client' : 'Save Changes'}
|
{saving ? 'Saving...' : mode === 'add' ? 'Add Client' : 'Save Changes'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export class DownloadClientManager {
|
|||||||
return new QBittorrentService(
|
return new QBittorrentService(
|
||||||
config.url,
|
config.url,
|
||||||
config.username || '',
|
config.username || '',
|
||||||
config.password,
|
config.password || '', // Optional for IP whitelist auth
|
||||||
'/downloads', // defaultSavePath
|
'/downloads', // defaultSavePath
|
||||||
config.category || 'readmeabook', // defaultCategory
|
config.category || 'readmeabook', // defaultCategory
|
||||||
config.disableSSLVerify,
|
config.disableSSLVerify,
|
||||||
|
|||||||
@@ -163,15 +163,15 @@ describe('Setup test routes', () => {
|
|||||||
expect(payload.error).toMatch(/Invalid client type/);
|
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 { POST } = await import('@/app/api/setup/test-download-client/route');
|
||||||
const response = await POST({
|
const response = await POST({
|
||||||
json: vi.fn().mockResolvedValue({ type: 'qbittorrent', url: 'http://qbt' }),
|
json: vi.fn().mockResolvedValue({ type: 'sabnzbd', url: 'http://sab' }),
|
||||||
} as any);
|
} as any);
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(payload.error).toMatch(/Username and password/);
|
expect(payload.error).toMatch(/API key/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tests SABnzbd connection', async () => {
|
it('tests SABnzbd connection', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user