/** * Component: Admin Download Client by ID API * Documentation: documentation/phase3/download-clients.md */ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { getConfigService } from '@/lib/services/config.service'; import { getDownloadClientManager, invalidateDownloadClientManager } from '@/lib/services/download-client-manager.service'; import { DownloadClientConfig } from '@/lib/services/download-client-manager.service'; import { getEncryptionService } from '@/lib/services/encryption.service'; import { RMABLogger } from '@/lib/utils/logger'; const logger = RMABLogger.create('API.Admin.Settings.DownloadClients.ID'); /** * PUT - Update download client by ID */ export async function PUT( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAdmin(req, async () => { try { // Await params in Next.js 15+ const { id } = await params; const body = await request.json(); const { name, enabled, url, username, password, disableSSLVerify, remotePathMappingEnabled, remotePath, localPath, category, customPath, postImportCategory, } = body; const config = await getConfigService(); const manager = getDownloadClientManager(config); const clients = await manager.getAllClients(); const clientIndex = clients.findIndex(c => c.id === id); if (clientIndex === -1) { return NextResponse.json( { error: 'Download client not found' }, { status: 404 } ); } const existingClient = clients[clientIndex]; // Validate customPath: reject paths containing ".." if (customPath && customPath.includes('..')) { return NextResponse.json( { error: 'Custom path cannot contain ".."' }, { status: 400 } ); } // Build updated client (preserve fields not in request) const updatedClient: DownloadClientConfig = { ...existingClient, name: name !== undefined ? name : existingClient.name, enabled: enabled !== undefined ? enabled : existingClient.enabled, url: url !== undefined ? url : existingClient.url, username: username !== undefined ? username : existingClient.username, password: password === '********' ? existingClient.password : (password || existingClient.password), disableSSLVerify: disableSSLVerify !== undefined ? disableSSLVerify : existingClient.disableSSLVerify, remotePathMappingEnabled: remotePathMappingEnabled !== undefined ? remotePathMappingEnabled : existingClient.remotePathMappingEnabled, remotePath: remotePath !== undefined ? remotePath : existingClient.remotePath, localPath: localPath !== undefined ? localPath : existingClient.localPath, category: category !== undefined ? category : existingClient.category, customPath: customPath !== undefined ? (customPath || undefined) : existingClient.customPath, postImportCategory: postImportCategory !== undefined ? (postImportCategory || undefined) : existingClient.postImportCategory, }; // Validate path mapping if enabled if (updatedClient.remotePathMappingEnabled) { if (!updatedClient.remotePath || !updatedClient.localPath) { return NextResponse.json( { error: 'Remote path and local path are required when path mapping is enabled' }, { status: 400 } ); } } // Test connection if credentials/URL changed (skip if disabling client) const isDisabling = enabled === false; if ( !isDisabling && ( url !== undefined || username !== undefined || (password && password !== '********') || disableSSLVerify !== undefined ) ) { const testResult = await manager.testConnection(updatedClient); if (!testResult.success) { return NextResponse.json( { error: `Connection test failed: ${testResult.message}` }, { status: 400 } ); } } // Update clients array and encrypt passwords before saving clients[clientIndex] = updatedClient; const encryptionService = getEncryptionService(); const encryptedClients = clients.map(c => ({ ...c, password: c.password ? encryptionService.encrypt(c.password) : '', })); await config.setMany([ { key: 'download_clients', value: JSON.stringify(encryptedClients) }, ]); // Invalidate cache invalidateDownloadClientManager(); logger.info('Download client updated', { id, type: updatedClient.type, name: updatedClient.name }); return NextResponse.json({ message: 'Download client updated successfully', client: { ...updatedClient, password: '********', }, }); } catch (error) { logger.error('Failed to update download client', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { error: 'Failed to update download client' }, { status: 500 } ); } }); }); } /** * DELETE - Remove download client by ID */ export async function DELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAdmin(req, async () => { try { // Await params in Next.js 15+ const { id } = await params; const config = await getConfigService(); const manager = getDownloadClientManager(config); const clients = await manager.getAllClients(); const clientIndex = clients.findIndex(c => c.id === id); if (clientIndex === -1) { return NextResponse.json( { error: 'Download client not found' }, { status: 404 } ); } const deletedClient = clients[clientIndex]; // Remove client from array and encrypt passwords before saving const updatedClients = clients.filter(c => c.id !== id); const encryptionService = getEncryptionService(); const encryptedClients = updatedClients.map(c => ({ ...c, password: c.password ? encryptionService.encrypt(c.password) : '', })); await config.setMany([ { key: 'download_clients', value: JSON.stringify(encryptedClients) }, ]); // Invalidate cache invalidateDownloadClientManager(); logger.info('Download client deleted', { id, type: deletedClient.type, name: deletedClient.name }); return NextResponse.json({ message: 'Download client deleted successfully', }); } catch (error) { logger.error('Failed to delete download client', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { error: 'Failed to delete download client' }, { status: 500 } ); } }); }); }