mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Add Transmission/NZBGet and per-client paths and much more
Extend multi-download-client support to include Transmission and NZBGet and introduce per-client custom download paths. Adds protocol mapping and new client types, Transmission/NZBGet integration services, API CRUD and validation changes, UI components/modal updates and live path previews, and manager routing by protocol. Includes DB migrations (download_path on download_history, interactive_search_access on users), schema updates, and related processor/service fixes and tests to ensure backward compatibility and proper path resolution.
This commit is contained in:
@@ -6,10 +6,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getQBittorrentService } from '@/lib/integrations/qbittorrent.service';
|
||||
import { getSABnzbdService } from '@/lib/integrations/sabnzbd.service';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { getDownloadClientManager } from '@/lib/services/download-client-manager.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
import { CLIENT_PROTOCOL_MAP, DownloadClientType } from '@/lib/interfaces/download-client.interface';
|
||||
|
||||
const logger = RMABLogger.create('API.Admin.Downloads');
|
||||
|
||||
@@ -55,6 +55,7 @@ export async function GET(request: NextRequest) {
|
||||
torrentName: true,
|
||||
torrentHash: true,
|
||||
nzbId: true,
|
||||
downloadClientId: true,
|
||||
downloadClient: true, // qbittorrent, sabnzbd, or direct
|
||||
torrentSizeBytes: true,
|
||||
startedAt: true,
|
||||
@@ -68,9 +69,9 @@ export async function GET(request: NextRequest) {
|
||||
take: 20,
|
||||
});
|
||||
|
||||
// Get configured download client type
|
||||
// Get download client manager
|
||||
const configService = getConfigService();
|
||||
const clientType = (await configService.get('download_client_type')) || 'qbittorrent';
|
||||
const manager = getDownloadClientManager(configService);
|
||||
|
||||
// Format response with speed and ETA from download client
|
||||
const formatted = await Promise.all(
|
||||
@@ -98,24 +99,19 @@ export async function GET(request: NextRequest) {
|
||||
eta = speed > 0 ? Math.round(remainingBytes / speed) : null;
|
||||
}
|
||||
}
|
||||
} else if (downloadClient === 'qbittorrent' || (!downloadClient && clientType === 'qbittorrent')) {
|
||||
// Get torrent hash from download history
|
||||
const torrentHash = downloadHistory?.torrentHash;
|
||||
if (torrentHash) {
|
||||
const qbService = await getQBittorrentService();
|
||||
const torrentInfo = await qbService.getTorrent(torrentHash);
|
||||
speed = torrentInfo.dlspeed;
|
||||
eta = torrentInfo.eta > 0 ? torrentInfo.eta : null;
|
||||
}
|
||||
} else if (downloadClient === 'sabnzbd' || (!downloadClient && clientType === 'sabnzbd')) {
|
||||
// Get NZB ID from download history
|
||||
const nzbId = downloadHistory?.nzbId;
|
||||
if (nzbId) {
|
||||
const sabnzbdService = await getSABnzbdService();
|
||||
const nzbInfo = await sabnzbdService.getNZB(nzbId);
|
||||
if (nzbInfo) {
|
||||
speed = nzbInfo.downloadSpeed;
|
||||
eta = nzbInfo.timeLeft > 0 ? nzbInfo.timeLeft : null;
|
||||
} else {
|
||||
// Use unified interface for all download clients (qBittorrent, SABnzbd, etc.)
|
||||
const clientId = downloadHistory?.downloadClientId || downloadHistory?.torrentHash || downloadHistory?.nzbId;
|
||||
if (clientId && downloadClient) {
|
||||
const protocol = CLIENT_PROTOCOL_MAP[downloadClient as DownloadClientType] || 'torrent';
|
||||
const client = await manager.getClientServiceForProtocol(protocol as 'torrent' | 'usenet');
|
||||
|
||||
if (client) {
|
||||
const info = await client.getDownload(clientId);
|
||||
if (info) {
|
||||
speed = info.downloadSpeed;
|
||||
eta = info.eta > 0 ? info.eta : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { getDownloadClientManager, invalidateDownloadClientManager, DownloadClientConfig } from '@/lib/services/download-client-manager.service';
|
||||
import { SUPPORTED_CLIENT_TYPES, getClientDisplayName } from '@/lib/interfaces/download-client.interface';
|
||||
import { PathMapper } from '@/lib/utils/path-mapper';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
import { randomUUID } from 'crypto';
|
||||
@@ -35,9 +36,9 @@ export async function PUT(request: NextRequest) {
|
||||
logger.warn('DEPRECATED: Using legacy single-client API. Please use /api/admin/settings/download-clients instead.');
|
||||
|
||||
// Validate type
|
||||
if (type !== 'qbittorrent' && type !== 'sabnzbd') {
|
||||
if (!SUPPORTED_CLIENT_TYPES.includes(type)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid client type. Must be qbittorrent or sabnzbd' },
|
||||
{ error: `Invalid client type. Must be one of: ${SUPPORTED_CLIENT_TYPES.join(', ')}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -97,7 +98,7 @@ export async function PUT(request: NextRequest) {
|
||||
const updatedClient: DownloadClientConfig = {
|
||||
id: existingIndex >= 0 ? existingClients[existingIndex].id : randomUUID(),
|
||||
type,
|
||||
name: type === 'qbittorrent' ? 'qBittorrent' : 'SABnzbd',
|
||||
name: getClientDisplayName(type),
|
||||
enabled: true,
|
||||
url,
|
||||
username: username || undefined,
|
||||
@@ -137,6 +138,12 @@ export async function PUT(request: NextRequest) {
|
||||
} else if (type === 'sabnzbd') {
|
||||
const { invalidateSABnzbdService } = await import('@/lib/integrations/sabnzbd.service');
|
||||
invalidateSABnzbdService();
|
||||
} else if (type === 'nzbget') {
|
||||
const { invalidateNZBGetService } = await import('@/lib/integrations/nzbget.service');
|
||||
invalidateNZBGetService();
|
||||
} else if (type === 'transmission') {
|
||||
const { invalidateTransmissionService } = await import('@/lib/integrations/transmission.service');
|
||||
invalidateTransmissionService();
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -37,6 +37,7 @@ export async function PUT(
|
||||
remotePath,
|
||||
localPath,
|
||||
category,
|
||||
customPath,
|
||||
} = body;
|
||||
|
||||
const config = await getConfigService();
|
||||
@@ -53,6 +54,14 @@ export async function PUT(
|
||||
|
||||
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,
|
||||
@@ -66,6 +75,7 @@ export async function PUT(
|
||||
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,
|
||||
};
|
||||
|
||||
// Validate path mapping if enabled
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
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 { getDownloadClientManager, invalidateDownloadClientManager, DownloadClientConfig } from '@/lib/services/download-client-manager.service';
|
||||
import { SUPPORTED_CLIENT_TYPES, CLIENT_PROTOCOL_MAP, DownloadClientType, getClientDisplayName } from '@/lib/interfaces/download-client.interface';
|
||||
import { getEncryptionService } from '@/lib/services/encryption.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
import { randomUUID } from 'crypto';
|
||||
@@ -62,12 +62,13 @@ export async function POST(request: NextRequest) {
|
||||
remotePath,
|
||||
localPath,
|
||||
category,
|
||||
customPath,
|
||||
} = body;
|
||||
|
||||
// Validate type
|
||||
if (type !== 'qbittorrent' && type !== 'sabnzbd') {
|
||||
if (!SUPPORTED_CLIENT_TYPES.includes(type)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid client type. Must be qbittorrent or sabnzbd' },
|
||||
{ error: `Invalid client type. Must be one of: ${SUPPORTED_CLIENT_TYPES.join(', ')}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -99,21 +100,30 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate type (only one client per type for now)
|
||||
// Check for duplicate protocol (only one client per protocol)
|
||||
const config = await getConfigService();
|
||||
const manager = getDownloadClientManager(config);
|
||||
const existingClients = await manager.getAllClients();
|
||||
|
||||
const duplicateType = existingClients.find(c => c.type === type && c.enabled);
|
||||
if (duplicateType) {
|
||||
const protocol = CLIENT_PROTOCOL_MAP[type as DownloadClientType];
|
||||
const duplicateProtocol = existingClients.find(c => CLIENT_PROTOCOL_MAP[c.type] === protocol);
|
||||
if (duplicateProtocol) {
|
||||
return NextResponse.json(
|
||||
{ error: `A ${type} client is already configured. Please disable or remove it first.` },
|
||||
{ error: `A ${protocol} client (${getClientDisplayName(duplicateProtocol.type)}) is already configured. Remove it first to add a different ${protocol} client.` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create new client config for testing (with plaintext password)
|
||||
// qBittorrent credentials are optional (supports IP whitelist auth)
|
||||
// Validate customPath: reject paths containing ".."
|
||||
if (customPath && customPath.includes('..')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Custom path cannot contain ".."' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const newClient: DownloadClientConfig = {
|
||||
id: randomUUID(),
|
||||
type,
|
||||
@@ -127,6 +137,7 @@ export async function POST(request: NextRequest) {
|
||||
remotePath: remotePath || undefined,
|
||||
localPath: localPath || undefined,
|
||||
category: category || 'readmeabook',
|
||||
customPath: customPath || undefined,
|
||||
};
|
||||
|
||||
// Test connection before saving
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { getDownloadClientManager } from '@/lib/services/download-client-manager.service';
|
||||
import { DownloadClientConfig } from '@/lib/services/download-client-manager.service';
|
||||
import { getDownloadClientManager, DownloadClientConfig } from '@/lib/services/download-client-manager.service';
|
||||
import { SUPPORTED_CLIENT_TYPES } from '@/lib/interfaces/download-client.interface';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Admin.Settings.DownloadClients.Test');
|
||||
@@ -23,6 +23,7 @@ export async function POST(request: NextRequest) {
|
||||
const {
|
||||
clientId, // Optional: existing client ID to use stored password
|
||||
type,
|
||||
name: clientName,
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
@@ -33,9 +34,9 @@ export async function POST(request: NextRequest) {
|
||||
} = body;
|
||||
|
||||
// Validate type
|
||||
if (type !== 'qbittorrent' && type !== 'sabnzbd') {
|
||||
if (!SUPPORTED_CLIENT_TYPES.includes(type)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid client type. Must be qbittorrent or sabnzbd' },
|
||||
{ error: `Invalid client type. Must be one of: ${SUPPORTED_CLIENT_TYPES.join(', ')}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -87,7 +88,7 @@ export async function POST(request: NextRequest) {
|
||||
const testConfig: DownloadClientConfig = {
|
||||
id: 'test',
|
||||
type,
|
||||
name: 'Test Client',
|
||||
name: clientName || type,
|
||||
enabled: true,
|
||||
url,
|
||||
username: effectiveUsername || '',
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Component: Admin Interactive Search Settings API
|
||||
* Documentation: documentation/settings-pages.md
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Admin.Settings.InteractiveSearch');
|
||||
|
||||
const CONFIG_KEY = 'interactive_search_access';
|
||||
|
||||
/**
|
||||
* GET /api/admin/settings/interactive-search
|
||||
* Get current global interactive search access setting
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
||||
return requireAdmin(req, async () => {
|
||||
try {
|
||||
const config = await prisma.configuration.findUnique({
|
||||
where: { key: CONFIG_KEY },
|
||||
});
|
||||
|
||||
// Default to true if not configured (backward compatibility)
|
||||
const interactiveSearchAccess = config === null ? true : config.value === 'true';
|
||||
|
||||
return NextResponse.json({ interactiveSearchAccess });
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch interactive search setting', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch interactive search setting' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/admin/settings/interactive-search
|
||||
* Update global interactive search access setting
|
||||
*/
|
||||
export async function PATCH(request: NextRequest) {
|
||||
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
||||
return requireAdmin(req, async () => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { interactiveSearchAccess } = body;
|
||||
|
||||
// Validate input
|
||||
if (typeof interactiveSearchAccess !== 'boolean') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input. interactiveSearchAccess must be a boolean' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Update configuration
|
||||
await prisma.configuration.upsert({
|
||||
where: { key: CONFIG_KEY },
|
||||
create: {
|
||||
key: CONFIG_KEY,
|
||||
value: interactiveSearchAccess.toString(),
|
||||
},
|
||||
update: {
|
||||
value: interactiveSearchAccess.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`Interactive search access setting updated to: ${interactiveSearchAccess}`, {
|
||||
userId: req.user?.sub,
|
||||
});
|
||||
|
||||
return NextResponse.json({ interactiveSearchAccess });
|
||||
} catch (error) {
|
||||
logger.error('Failed to update interactive search setting', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update interactive search setting' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export async function PUT(request: NextRequest) {
|
||||
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
||||
return requireAdmin(req, async () => {
|
||||
try {
|
||||
const { downloadDir, mediaDir, audiobookPathTemplate, metadataTaggingEnabled, chapterMergingEnabled } = await request.json();
|
||||
const { downloadDir, mediaDir, audiobookPathTemplate, ebookPathTemplate, metadataTaggingEnabled, chapterMergingEnabled } = await request.json();
|
||||
|
||||
if (!downloadDir || !mediaDir) {
|
||||
return NextResponse.json(
|
||||
@@ -59,6 +59,20 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
// Update ebook path template
|
||||
if (ebookPathTemplate !== undefined) {
|
||||
await prisma.configuration.upsert({
|
||||
where: { key: 'ebook_path_template' },
|
||||
update: { value: ebookPathTemplate },
|
||||
create: {
|
||||
key: 'ebook_path_template',
|
||||
value: ebookPathTemplate,
|
||||
category: 'automation',
|
||||
description: 'Template for organizing ebook files in media directory',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Update metadata tagging setting
|
||||
await prisma.configuration.upsert({
|
||||
where: { key: 'metadata_tagging_enabled' },
|
||||
@@ -90,12 +104,21 @@ export async function PUT(request: NextRequest) {
|
||||
configService.clearCache('download_dir');
|
||||
configService.clearCache('media_dir');
|
||||
configService.clearCache('audiobook_path_template');
|
||||
configService.clearCache('ebook_path_template');
|
||||
configService.clearCache('metadata_tagging_enabled');
|
||||
configService.clearCache('chapter_merging_enabled');
|
||||
|
||||
// Invalidate qBittorrent service singleton to force reload of download_dir
|
||||
// Invalidate all download client singletons to force reload of download_dir
|
||||
const { invalidateDownloadClientManager } = await import('@/lib/services/download-client-manager.service');
|
||||
invalidateDownloadClientManager();
|
||||
const { invalidateQBittorrentService } = await import('@/lib/integrations/qbittorrent.service');
|
||||
invalidateQBittorrentService();
|
||||
const { invalidateSABnzbdService } = await import('@/lib/integrations/sabnzbd.service');
|
||||
invalidateSABnzbdService();
|
||||
const { invalidateNZBGetService } = await import('@/lib/integrations/nzbget.service');
|
||||
invalidateNZBGetService();
|
||||
const { invalidateTransmissionService } = await import('@/lib/integrations/transmission.service');
|
||||
invalidateTransmissionService();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -125,6 +125,7 @@ export async function GET(request: NextRequest) {
|
||||
downloadDir: configMap.get('download_dir') || '/downloads',
|
||||
mediaDir: configMap.get('media_dir') || '/media/audiobooks',
|
||||
audiobookPathTemplate: configMap.get('audiobook_path_template') || '{author}/{title} {asin}',
|
||||
ebookPathTemplate: configMap.get('ebook_path_template') || configMap.get('audiobook_path_template') || '{author}/{title} {asin}',
|
||||
metadataTaggingEnabled: configMap.get('metadata_tagging_enabled') === 'true',
|
||||
chapterMergingEnabled: configMap.get('chapter_merging_enabled') === 'true',
|
||||
},
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/**
|
||||
* Component: Admin Settings Test Download Client API
|
||||
* Component: Admin Settings Test Download Client API (DEPRECATED)
|
||||
* Documentation: documentation/settings-pages.md
|
||||
*
|
||||
* DEPRECATED: Use /api/admin/settings/download-clients/test instead.
|
||||
* Maintained for backward compatibility.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { getDownloadClientManager } from '@/lib/services/download-client-manager.service';
|
||||
import { QBittorrentService } from '@/lib/integrations/qbittorrent.service';
|
||||
import { SABnzbdService } from '@/lib/integrations/sabnzbd.service';
|
||||
import { getDownloadClientManager, DownloadClientConfig } from '@/lib/services/download-client-manager.service';
|
||||
import { SUPPORTED_CLIENT_TYPES } from '@/lib/interfaces/download-client.interface';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.TestDownloadClient');
|
||||
@@ -19,6 +21,7 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const {
|
||||
type,
|
||||
name: clientName,
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
@@ -37,9 +40,9 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
if (type !== 'qbittorrent' && type !== 'sabnzbd') {
|
||||
if (!SUPPORTED_CLIENT_TYPES.includes(type)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid client type. Must be qbittorrent or sabnzbd' },
|
||||
{ success: false, error: `Invalid client type. Must be one of: ${SUPPORTED_CLIENT_TYPES.join(', ')}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -64,53 +67,28 @@ export async function POST(request: NextRequest) {
|
||||
actualPassword = matchingClient.password;
|
||||
}
|
||||
|
||||
// Validate required fields per client type and test connection
|
||||
let version: string | undefined;
|
||||
// Build a temporary config for testing
|
||||
const testConfig: DownloadClientConfig = {
|
||||
id: 'legacy-test',
|
||||
type,
|
||||
name: clientName || type,
|
||||
enabled: true,
|
||||
url,
|
||||
username: username || '',
|
||||
password: actualPassword || '',
|
||||
disableSSLVerify: disableSSLVerify || false,
|
||||
remotePathMappingEnabled: remotePathMappingEnabled || false,
|
||||
remotePath: remotePath || undefined,
|
||||
localPath: localPath || undefined,
|
||||
category: 'readmeabook',
|
||||
};
|
||||
|
||||
if (type === 'qbittorrent') {
|
||||
logger.debug('Testing qBittorrent connection');
|
||||
if (!username || !actualPassword) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Username and password are required for qBittorrent' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Test qBittorrent connection
|
||||
version = await QBittorrentService.testConnectionWithCredentials(
|
||||
url,
|
||||
username,
|
||||
actualPassword,
|
||||
disableSSLVerify || false
|
||||
);
|
||||
} else if (type === 'sabnzbd') {
|
||||
logger.debug('Testing SABnzbd connection');
|
||||
if (!actualPassword) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'API key (password) is required for SABnzbd' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Test SABnzbd connection
|
||||
const sabnzbd = new SABnzbdService(url, actualPassword, 'readmeabook', disableSSLVerify || false);
|
||||
const result = await sabnzbd.testConnection();
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: result.error || 'Failed to connect to SABnzbd',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
version = result.version;
|
||||
}
|
||||
const configService = getConfigService();
|
||||
const manager = getDownloadClientManager(configService);
|
||||
const result = await manager.testConnection(testConfig);
|
||||
|
||||
// If path mapping enabled, validate local path exists
|
||||
if (remotePathMappingEnabled) {
|
||||
if (result.success && remotePathMappingEnabled) {
|
||||
if (!remotePath || !localPath) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -136,10 +114,14 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
version,
|
||||
});
|
||||
if (result.success) {
|
||||
return NextResponse.json({ success: true, message: result.message });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.message },
|
||||
{ status: 400 }
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Download client test failed', { error: error instanceof Error ? error.message : String(error) });
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function PUT(
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { role, autoApproveRequests } = body;
|
||||
const { role, autoApproveRequests, interactiveSearchAccess } = body;
|
||||
|
||||
// Validate role
|
||||
if (!role || (role !== 'user' && role !== 'admin')) {
|
||||
@@ -37,6 +37,14 @@ export async function PUT(
|
||||
);
|
||||
}
|
||||
|
||||
// Validate interactiveSearchAccess (optional)
|
||||
if (interactiveSearchAccess !== undefined && interactiveSearchAccess !== null && typeof interactiveSearchAccess !== 'boolean') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid interactiveSearchAccess. Must be a boolean or null' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent user from demoting themselves
|
||||
if (req.user && id === req.user.sub) {
|
||||
return NextResponse.json(
|
||||
@@ -91,21 +99,30 @@ export async function PUT(
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that admins cannot have autoApproveRequests set to false
|
||||
// Validate that admins cannot have permissions set to false
|
||||
if (role === 'admin' && autoApproveRequests === false) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Admins must always auto-approve requests. Cannot set autoApproveRequests to false for admin users.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (role === 'admin' && interactiveSearchAccess === false) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Admins always have interactive search access. Cannot set interactiveSearchAccess to false for admin users.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
const updateData: { role: string; autoApproveRequests?: boolean | null } = { role };
|
||||
const updateData: { role: string; autoApproveRequests?: boolean | null; interactiveSearchAccess?: boolean | null } = { role };
|
||||
if (autoApproveRequests !== undefined) {
|
||||
updateData.autoApproveRequests = autoApproveRequests;
|
||||
}
|
||||
if (interactiveSearchAccess !== undefined) {
|
||||
updateData.interactiveSearchAccess = interactiveSearchAccess;
|
||||
}
|
||||
|
||||
// Update user role and autoApproveRequests
|
||||
// Update user
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
@@ -114,6 +131,7 @@ export async function PUT(
|
||||
plexUsername: true,
|
||||
role: true,
|
||||
autoApproveRequests: true,
|
||||
interactiveSearchAccess: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export async function GET(request: NextRequest) {
|
||||
updatedAt: true,
|
||||
lastLoginAt: true,
|
||||
autoApproveRequests: true,
|
||||
interactiveSearchAccess: true,
|
||||
_count: {
|
||||
select: {
|
||||
requests: true,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { groupIndexersByCategories } from '@/lib/utils/indexer-grouping';
|
||||
import { findPlexMatch } from '@/lib/utils/audiobook-matcher';
|
||||
import { getAudibleService } from '@/lib/integrations/audible.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
import { resolveInteractiveSearchAccess } from '@/lib/utils/permissions';
|
||||
import {
|
||||
searchByAsin,
|
||||
searchByTitle,
|
||||
@@ -83,6 +84,21 @@ export async function POST(
|
||||
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
||||
try {
|
||||
const { asin } = await params;
|
||||
|
||||
// Check interactive search access permission
|
||||
if (req.user) {
|
||||
const callingUser = await prisma.user.findUnique({
|
||||
where: { id: req.user.id },
|
||||
select: { role: true, interactiveSearchAccess: true },
|
||||
});
|
||||
if (!callingUser || !(await resolveInteractiveSearchAccess(callingUser.role, callingUser.interactiveSearchAccess))) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Forbidden', message: 'You do not have interactive search access. Contact your admin to enable this permission.' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const customTitle = body.customTitle as string | undefined;
|
||||
|
||||
@@ -410,9 +426,14 @@ async function searchIndexersForInteractive(
|
||||
const flagConfigs = flagConfigStr ? JSON.parse(flagConfigStr) : [];
|
||||
|
||||
// Group indexers by ebook categories
|
||||
const groups = groupIndexersByCategories(indexersConfig, 'ebook');
|
||||
const { groups, skippedIndexers } = groupIndexersByCategories(indexersConfig, 'ebook');
|
||||
|
||||
logger.info(`Searching ${indexersConfig.length} indexers in ${groups.length} group(s)`);
|
||||
if (skippedIndexers.length > 0) {
|
||||
const skippedNames = skippedIndexers.map(idx => idx.name).join(', ');
|
||||
logger.info(`Skipping ${skippedIndexers.length} indexer(s) with no ebook categories: ${skippedNames}`);
|
||||
}
|
||||
|
||||
logger.info(`Searching ${indexersConfig.length - skippedIndexers.length} indexers in ${groups.length} group(s)`);
|
||||
|
||||
// Get Prowlarr service
|
||||
const prowlarr = await getProwlarrService();
|
||||
|
||||
@@ -70,9 +70,14 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Group indexers by their category configuration
|
||||
// This minimizes API calls while ensuring each indexer only searches its configured categories
|
||||
const groups = groupIndexersByCategories(indexersConfig);
|
||||
const { groups, skippedIndexers } = groupIndexersByCategories(indexersConfig);
|
||||
|
||||
logger.info(`Searching ${indexersConfig.length} enabled indexers in ${groups.length} group${groups.length > 1 ? 's' : ''}`, { searchQuery: title });
|
||||
if (skippedIndexers.length > 0) {
|
||||
const skippedNames = skippedIndexers.map(idx => idx.name).join(', ');
|
||||
logger.info(`Skipping ${skippedIndexers.length} indexer(s) with no audiobook categories: ${skippedNames}`);
|
||||
}
|
||||
|
||||
logger.info(`Searching ${indexersConfig.length - skippedIndexers.length} enabled indexers in ${groups.length} group${groups.length > 1 ? 's' : ''}`, { searchQuery: title });
|
||||
|
||||
// Log each group for transparency
|
||||
groups.forEach((group, index) => {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { resolvePermission, getGlobalBooleanSetting } from '@/lib/utils/permissions';
|
||||
|
||||
/**
|
||||
* GET /api/auth/me
|
||||
@@ -37,6 +38,7 @@ export async function GET(request: NextRequest) {
|
||||
authProvider: true,
|
||||
createdAt: true,
|
||||
lastLoginAt: true,
|
||||
interactiveSearchAccess: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,6 +55,14 @@ export async function GET(request: NextRequest) {
|
||||
// Determine if user is local admin (setup admin with local authentication)
|
||||
const isLocalAdmin = user.isSetupAdmin && user.plexId.startsWith('local-');
|
||||
|
||||
// Resolve effective permissions
|
||||
const globalInteractiveSearch = await getGlobalBooleanSetting('interactive_search_access', true);
|
||||
const effectiveInteractiveSearch = resolvePermission(
|
||||
user.role,
|
||||
user.interactiveSearchAccess,
|
||||
globalInteractiveSearch
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
@@ -65,6 +75,9 @@ export async function GET(request: NextRequest) {
|
||||
authProvider: user.authProvider,
|
||||
createdAt: user.createdAt,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
permissions: {
|
||||
interactiveSearch: effectiveInteractiveSearch,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -321,9 +321,14 @@ async function searchIndexersForInteractive(
|
||||
const flagConfigs = flagConfigStr ? JSON.parse(flagConfigStr) : [];
|
||||
|
||||
// Group indexers by ebook categories
|
||||
const groups = groupIndexersByCategories(indexersConfig, 'ebook');
|
||||
const { groups, skippedIndexers } = groupIndexersByCategories(indexersConfig, 'ebook');
|
||||
|
||||
logger.info(`Searching ${indexersConfig.length} indexers in ${groups.length} group(s)`);
|
||||
if (skippedIndexers.length > 0) {
|
||||
const skippedNames = skippedIndexers.map(idx => idx.name).join(', ');
|
||||
logger.info(`Skipping ${skippedIndexers.length} indexer(s) with no ebook categories: ${skippedNames}`);
|
||||
}
|
||||
|
||||
logger.info(`Searching ${indexersConfig.length - skippedIndexers.length} indexers in ${groups.length} group(s)`);
|
||||
|
||||
// Get Prowlarr service
|
||||
const prowlarr = await getProwlarrService();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { prisma } from '@/lib/db';
|
||||
import { getProwlarrService } from '@/lib/integrations/prowlarr.service';
|
||||
import { rankTorrents } from '@/lib/utils/ranking-algorithm';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
import { resolveInteractiveSearchAccess } from '@/lib/utils/permissions';
|
||||
|
||||
const logger = RMABLogger.create('API.InteractiveSearch');
|
||||
|
||||
@@ -71,6 +72,18 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
// Check interactive search access permission
|
||||
const callingUser = await prisma.user.findUnique({
|
||||
where: { id: req.user.id },
|
||||
select: { role: true, interactiveSearchAccess: true },
|
||||
});
|
||||
if (!callingUser || !(await resolveInteractiveSearchAccess(callingUser.role, callingUser.interactiveSearchAccess))) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Forbidden', message: 'You do not have interactive search access. Contact your admin to enable this permission.' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get enabled indexers from configuration
|
||||
const { getConfigService } = await import('@/lib/services/config.service');
|
||||
const configService = getConfigService();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
import { CLIENT_PROTOCOL_MAP, DownloadClientType } from '@/lib/interfaces/download-client.interface';
|
||||
|
||||
const logger = RMABLogger.create('API.RequestById');
|
||||
|
||||
@@ -200,28 +201,11 @@ export async function PATCH(
|
||||
// Get download path from the appropriate download client
|
||||
let downloadPath: string;
|
||||
|
||||
if (downloadHistory.torrentHash) {
|
||||
// qBittorrent - get path from torrent info
|
||||
const { getQBittorrentService } = await import('@/lib/integrations/qbittorrent.service');
|
||||
const qbt = await getQBittorrentService();
|
||||
const torrent = await qbt.getTorrent(downloadHistory.torrentHash);
|
||||
downloadPath = `${torrent.save_path}/${torrent.name}`;
|
||||
} else if (downloadHistory.nzbId) {
|
||||
// SABnzbd - get path from NZB info
|
||||
const { getSABnzbdService } = await import('@/lib/integrations/sabnzbd.service');
|
||||
const sabnzbd = await getSABnzbdService();
|
||||
const nzbInfo = await sabnzbd.getNZB(downloadHistory.nzbId);
|
||||
if (!nzbInfo || !nzbInfo.downloadPath) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'ValidationError',
|
||||
message: 'Download path not available from SABnzbd',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
downloadPath = nzbInfo.downloadPath;
|
||||
} else {
|
||||
// Get download path via unified interface
|
||||
const clientId = downloadHistory.downloadClientId || downloadHistory.torrentHash || downloadHistory.nzbId;
|
||||
const clientType = downloadHistory.downloadClient || 'qbittorrent';
|
||||
|
||||
if (!clientId || clientType === 'direct') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'ValidationError',
|
||||
@@ -231,6 +215,35 @@ export async function PATCH(
|
||||
);
|
||||
}
|
||||
|
||||
const { getConfigService } = await import('@/lib/services/config.service');
|
||||
const { getDownloadClientManager } = await import('@/lib/services/download-client-manager.service');
|
||||
const configService = getConfigService();
|
||||
const manager = getDownloadClientManager(configService);
|
||||
const protocol = CLIENT_PROTOCOL_MAP[clientType as DownloadClientType] || 'torrent';
|
||||
const client = await manager.getClientServiceForProtocol(protocol as 'torrent' | 'usenet');
|
||||
|
||||
if (!client) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'ValidationError',
|
||||
message: `No ${clientType} client configured`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const info = await client.getDownload(clientId);
|
||||
if (!info?.downloadPath) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'ValidationError',
|
||||
message: `Download path not available from ${client.clientType}`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
downloadPath = info.downloadPath;
|
||||
|
||||
await jobQueue.addOrganizeJob(
|
||||
id,
|
||||
requestWithData.audiobook.id,
|
||||
|
||||
@@ -9,11 +9,14 @@ import bcrypt from 'bcrypt';
|
||||
import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt';
|
||||
import { getEncryptionService } from '@/lib/services/encryption.service';
|
||||
import { getPlexService } from '@/lib/integrations/plex.service';
|
||||
import { getClientDisplayName } from '@/lib/interfaces/download-client.interface';
|
||||
import { requireSetupIncomplete } from '@/lib/middleware/auth';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Setup.Complete');
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return requireSetupIncomplete(request, async (req) => {
|
||||
try {
|
||||
const {
|
||||
backendMode,
|
||||
@@ -28,7 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
downloadClient,
|
||||
paths,
|
||||
bookdate,
|
||||
} = await request.json();
|
||||
} = await req.json();
|
||||
|
||||
// Validate backend mode
|
||||
if (!backendMode || !['plex', 'audiobookshelf'].includes(backendMode)) {
|
||||
@@ -401,7 +404,7 @@ export async function POST(request: NextRequest) {
|
||||
downloadClientsArray = [{
|
||||
id: `temp-${Date.now()}`,
|
||||
type: downloadClient.type,
|
||||
name: downloadClient.type === 'qbittorrent' ? 'qBittorrent' : 'SABnzbd',
|
||||
name: getClientDisplayName(downloadClient.type),
|
||||
enabled: true,
|
||||
url: downloadClient.url,
|
||||
username: downloadClient.username,
|
||||
@@ -562,4 +565,5 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireSetupIncomplete } from '@/lib/middleware/auth';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return requireSetupIncomplete(request, async (req) => {
|
||||
try {
|
||||
const { serverUrl, apiToken } = await request.json();
|
||||
const { serverUrl, apiToken } = await req.json();
|
||||
|
||||
if (!serverUrl) {
|
||||
return NextResponse.json(
|
||||
@@ -79,4 +81,5 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,15 +4,18 @@
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { QBittorrentService } from '@/lib/integrations/qbittorrent.service';
|
||||
import { SABnzbdService } from '@/lib/integrations/sabnzbd.service';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { getDownloadClientManager, DownloadClientConfig } from '@/lib/services/download-client-manager.service';
|
||||
import { SUPPORTED_CLIENT_TYPES } from '@/lib/interfaces/download-client.interface';
|
||||
import { requireSetupIncomplete } from '@/lib/middleware/auth';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Setup.TestDownloadClient');
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return requireSetupIncomplete(request, async (req) => {
|
||||
try {
|
||||
const { type, url, username, password, disableSSLVerify } = await request.json();
|
||||
const { type, name, url, username, password, disableSSLVerify } = await req.json();
|
||||
|
||||
if (!type || !url) {
|
||||
return NextResponse.json(
|
||||
@@ -21,59 +24,39 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
if (type !== 'qbittorrent' && type !== 'sabnzbd') {
|
||||
if (!SUPPORTED_CLIENT_TYPES.includes(type)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid client type. Must be qbittorrent or sabnzbd' },
|
||||
{ success: false, error: `Invalid client type. Must be one of: ${SUPPORTED_CLIENT_TYPES.join(', ')}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate required fields per client type
|
||||
// qBittorrent credentials are optional (supports IP whitelist auth)
|
||||
if (type === 'qbittorrent') {
|
||||
// Test qBittorrent connection (empty credentials work with IP whitelist)
|
||||
const version = await QBittorrentService.testConnectionWithCredentials(
|
||||
url,
|
||||
username || '',
|
||||
password || '',
|
||||
disableSSLVerify || false
|
||||
);
|
||||
// Build a temporary config for testing
|
||||
const testConfig: DownloadClientConfig = {
|
||||
id: 'setup-test',
|
||||
type,
|
||||
name: name || type,
|
||||
enabled: true,
|
||||
url,
|
||||
username: username || '',
|
||||
password: password || '',
|
||||
disableSSLVerify: disableSSLVerify || false,
|
||||
remotePathMappingEnabled: false,
|
||||
};
|
||||
|
||||
const configService = getConfigService();
|
||||
const manager = getDownloadClientManager(configService);
|
||||
const result = await manager.testConnection(testConfig);
|
||||
|
||||
if (result.success) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
version,
|
||||
});
|
||||
} else if (type === 'sabnzbd') {
|
||||
if (!password) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'API key (password) is required for SABnzbd' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Test SABnzbd connection
|
||||
const sabnzbd = new SABnzbdService(url, password, 'readmeabook', disableSSLVerify || false);
|
||||
const result = await sabnzbd.testConnection();
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: result.error || 'Failed to connect to SABnzbd',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
version: result.version,
|
||||
message: result.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Should never reach here
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid client type' },
|
||||
{ success: false, error: result.message },
|
||||
{ status: 400 }
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -86,4 +69,5 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { Issuer } from 'openid-client';
|
||||
import { requireSetupIncomplete } from '@/lib/middleware/auth';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Setup.TestOIDC');
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return requireSetupIncomplete(request, async (req) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const body = await req.json();
|
||||
const { issuerUrl, clientId, clientSecret } = body;
|
||||
|
||||
// Validate required fields
|
||||
@@ -93,4 +95,5 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { requireSetupIncomplete } from '@/lib/middleware/auth';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
import { validateTemplate, generateMockPreviews } from '@/lib/utils/path-template.util';
|
||||
|
||||
@@ -45,8 +46,9 @@ async function testPath(dirPath: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return requireSetupIncomplete(request, async (req) => {
|
||||
try {
|
||||
const { downloadDir, mediaDir, audiobookPathTemplate } = await request.json();
|
||||
const { downloadDir, mediaDir, audiobookPathTemplate } = await req.json();
|
||||
|
||||
if (!downloadDir || !mediaDir) {
|
||||
return NextResponse.json(
|
||||
@@ -126,4 +128,5 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getPlexService } from '@/lib/integrations/plex.service';
|
||||
import { requireSetupIncomplete } from '@/lib/middleware/auth';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Setup.TestPlex');
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return requireSetupIncomplete(request, async (req) => {
|
||||
try {
|
||||
const { url, token } = await request.json();
|
||||
const { url, token } = await req.json();
|
||||
|
||||
if (!url || !token) {
|
||||
return NextResponse.json(
|
||||
@@ -61,4 +63,5 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ProwlarrService } from '@/lib/integrations/prowlarr.service';
|
||||
import { requireSetupIncomplete } from '@/lib/middleware/auth';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Setup.TestProwlarr');
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return requireSetupIncomplete(request, async (req) => {
|
||||
try {
|
||||
const { url, apiKey } = await request.json();
|
||||
const { url, apiKey } = await req.json();
|
||||
|
||||
if (!url || !apiKey) {
|
||||
return NextResponse.json(
|
||||
@@ -50,4 +52,5 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user