Initial commit

This commit is contained in:
kikootwo
2026-01-28 11:41:24 -05:00
commit a3ba192fbd
257 changed files with 89482 additions and 0 deletions
@@ -0,0 +1,66 @@
/**
* Audiobookshelf Libraries API
* Documentation: documentation/features/audiobookshelf-integration.md
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
export async function GET(request: NextRequest) {
console.log('[ABS Libraries] GET request received');
return requireAuth(request, async (req: AuthenticatedRequest) => {
console.log('[ABS Libraries] Auth passed, user:', req.user);
return requireAdmin(req, async () => {
console.log('[ABS Libraries] Admin check passed');
try {
// Use getConfigService like Plex endpoint does
const { getConfigService } = await import('@/lib/services/config.service');
const configService = getConfigService();
const serverUrl = await configService.get('audiobookshelf.server_url');
const apiToken = await configService.get('audiobookshelf.api_token');
console.log('[ABS Libraries] Config loaded:', { hasServerUrl: !!serverUrl, hasApiToken: !!apiToken });
if (!serverUrl || !apiToken) {
return NextResponse.json(
{ error: 'Audiobookshelf not configured' },
{ status: 400 }
);
}
// Fetch libraries from Audiobookshelf
const response = await fetch(`${serverUrl.replace(/\/$/, '')}/api/libraries`, {
headers: {
'Authorization': `Bearer ${apiToken}`,
},
});
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch libraries from Audiobookshelf' },
{ status: response.status }
);
}
const data = await response.json();
// Filter to only audiobook libraries and map to expected format
const libraries = (data.libraries || [])
.filter((lib: any) => lib.mediaType === 'book')
.map((lib: any) => ({
id: lib.id,
name: lib.name,
type: lib.mediaType,
itemCount: lib.stats?.totalItems || 0,
}));
return NextResponse.json({ libraries });
} catch (error) {
console.error('[Admin] Failed to fetch ABS libraries:', error);
return NextResponse.json(
{ error: 'Failed to fetch libraries' },
{ status: 500 }
);
}
});
});
}
@@ -0,0 +1,51 @@
/**
* Audiobookshelf Settings API
* Documentation: documentation/features/audiobookshelf-integration.md
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { ConfigUpdate } from '@/lib/services/config.service';
export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const body = await request.json();
const { serverUrl, apiToken, libraryId } = body;
const { getConfigService } = await import('@/lib/services/config.service');
const configService = getConfigService();
// Build updates array, skipping masked values
const updates: ConfigUpdate[] = [
{ key: 'audiobookshelf.server_url', value: serverUrl || '' },
{ key: 'audiobookshelf.library_id', value: libraryId || '' },
];
// Only update API token if it's not the masked placeholder
if (apiToken && !apiToken.startsWith('••••')) {
updates.push({
key: 'audiobookshelf.api_token',
value: apiToken,
encrypted: true,
});
}
// Update configuration
await configService.setMany(updates);
return NextResponse.json({
success: true,
message: 'Audiobookshelf settings saved successfully'
});
} catch (error) {
console.error('[Admin] Failed to save Audiobookshelf settings:', error);
return NextResponse.json(
{ error: 'Failed to save settings' },
{ status: 500 }
);
}
});
});
}
@@ -0,0 +1,135 @@
/**
* Component: Local Admin Password Change API
* Documentation: documentation/backend/services/auth.md
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireLocalAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db';
import bcrypt from 'bcrypt';
/**
* POST /api/admin/settings/change-password
* Change password for local admin user
*
* Security:
* - Only available to local admin (isSetupAdmin=true AND plexId starts with 'local-')
* - Requires current password verification
* - New password must be at least 8 characters
* - New password must be different from current password
*/
export async function POST(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireLocalAdmin(req, async (authenticatedReq: AuthenticatedRequest) => {
try {
const { currentPassword, newPassword, confirmPassword } = await request.json();
// Validate input
if (!currentPassword || !newPassword || !confirmPassword) {
return NextResponse.json(
{
success: false,
error: 'All fields are required',
},
{ status: 400 }
);
}
// Validate new password length
if (newPassword.length < 8) {
return NextResponse.json(
{
success: false,
error: 'New password must be at least 8 characters',
},
{ status: 400 }
);
}
// Validate passwords match
if (newPassword !== confirmPassword) {
return NextResponse.json(
{
success: false,
error: 'New passwords do not match',
},
{ status: 400 }
);
}
// Validate new password is different from current
if (currentPassword === newPassword) {
return NextResponse.json(
{
success: false,
error: 'New password must be different from current password',
},
{ status: 400 }
);
}
// Get user from database
const user = await prisma.user.findUnique({
where: { id: authenticatedReq.user!.id },
select: {
id: true,
authToken: true,
plexId: true,
isSetupAdmin: true,
},
});
if (!user || !user.authToken) {
return NextResponse.json(
{
success: false,
error: 'User not found or invalid account type',
},
{ status: 404 }
);
}
// Verify current password
const currentPasswordValid = await bcrypt.compare(currentPassword, user.authToken);
if (!currentPasswordValid) {
return NextResponse.json(
{
success: false,
error: 'Current password is incorrect',
},
{ status: 400 }
);
}
// Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Update password in database
await prisma.user.update({
where: { id: user.id },
data: {
authToken: hashedPassword,
updatedAt: new Date(),
},
});
console.log(`[Auth] Local admin password changed successfully for user ${user.id}`);
return NextResponse.json({
success: true,
message: 'Password changed successfully',
});
} catch (error) {
console.error('[Auth] Failed to change password:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to change password',
},
{ status: 500 }
);
}
});
});
}
@@ -0,0 +1,77 @@
/**
* Component: Admin Download Client 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';
export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { type, url, username, password } = await request.json();
if (!type || !url || !username || !password) {
return NextResponse.json(
{ error: 'Type, URL, username, and password are required' },
{ status: 400 }
);
}
// Validate type
if (type !== 'qbittorrent' && type !== 'transmission') {
return NextResponse.json(
{ error: 'Invalid client type. Must be qbittorrent or transmission' },
{ status: 400 }
);
}
// Update configuration
await prisma.configuration.upsert({
where: { key: 'download_client_type' },
update: { value: type },
create: { key: 'download_client_type', value: type },
});
await prisma.configuration.upsert({
where: { key: 'download_client_url' },
update: { value: url },
create: { key: 'download_client_url', value: url },
});
await prisma.configuration.upsert({
where: { key: 'download_client_username' },
update: { value: username },
create: { key: 'download_client_username', value: username },
});
// Only update password if it's not the masked value
if (!password.startsWith('••••')) {
await prisma.configuration.upsert({
where: { key: 'download_client_password' },
update: { value: password },
create: { key: 'download_client_password', value: password },
});
}
console.log('[Admin] Download client settings updated');
return NextResponse.json({
success: true,
message: 'Download client settings updated successfully',
});
} catch (error) {
console.error('[Admin] Failed to update download client settings:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to update settings',
},
{ status: 500 }
);
}
});
});
}
+51
View File
@@ -0,0 +1,51 @@
/**
* OIDC Settings API
* Documentation: documentation/features/audiobookshelf-integration.md
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const body = await request.json();
const { enabled, providerName, issuerUrl, clientId, clientSecret } = body;
const { getConfigService } = await import('@/lib/services/config.service');
const configService = getConfigService();
// Build config updates
const updates: Array<{key: string; value: string; encrypted?: boolean}> = [
{ key: 'oidc.enabled', value: enabled ? 'true' : 'false' },
{ key: 'oidc.provider_name', value: providerName || '' },
{ key: 'oidc.issuer_url', value: issuerUrl || '' },
{ key: 'oidc.client_id', value: clientId || '' },
];
// Only update client secret if provided (not masked)
if (clientSecret && !clientSecret.includes('••')) {
updates.push({
key: 'oidc.client_secret',
value: clientSecret,
encrypted: true
});
}
await configService.setMany(updates);
return NextResponse.json({
success: true,
message: 'OIDC settings saved successfully'
});
} catch (error) {
console.error('[Admin] Failed to save OIDC settings:', error);
return NextResponse.json(
{ error: 'Failed to save settings' },
{ status: 500 }
);
}
});
});
}
+74
View File
@@ -0,0 +1,74 @@
/**
* Component: Admin Paths 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';
export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { downloadDir, mediaDir, metadataTaggingEnabled } = await request.json();
if (!downloadDir || !mediaDir) {
return NextResponse.json(
{ error: 'Download directory and media directory are required' },
{ status: 400 }
);
}
// Validate paths are not the same
if (downloadDir === mediaDir) {
return NextResponse.json(
{ error: 'Download and media directories must be different' },
{ status: 400 }
);
}
// Update configuration
await prisma.configuration.upsert({
where: { key: 'download_dir' },
update: { value: downloadDir },
create: { key: 'download_dir', value: downloadDir },
});
await prisma.configuration.upsert({
where: { key: 'media_dir' },
update: { value: mediaDir },
create: { key: 'media_dir', value: mediaDir },
});
// Update metadata tagging setting
await prisma.configuration.upsert({
where: { key: 'metadata_tagging_enabled' },
update: { value: String(metadataTaggingEnabled ?? true) },
create: {
key: 'metadata_tagging_enabled',
value: String(metadataTaggingEnabled ?? true),
category: 'automation',
description: 'Automatically tag audio files with correct metadata during file organization',
},
});
console.log('[Admin] Paths settings updated');
return NextResponse.json({
success: true,
message: 'Paths settings updated successfully',
});
} catch (error) {
console.error('[Admin] Failed to update paths settings:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to update settings',
},
{ status: 500 }
);
}
});
});
}
@@ -0,0 +1,66 @@
/**
* Component: Plex Libraries API Route
* Documentation: documentation/backend/api.md
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { getPlexService } from '@/lib/integrations/plex.service';
/**
* GET /api/admin/settings/plex/libraries
* Fetch available Plex libraries
*/
export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const plexService = await getPlexService();
// Get Plex configuration
const { getConfigService } = await import('@/lib/services/config.service');
const configService = getConfigService();
const plexUrl = await configService.get('plex_url');
const plexToken = await configService.get('plex_token');
if (!plexUrl || !plexToken) {
return NextResponse.json(
{
success: false,
error: 'Plex not configured',
message: 'Please configure Plex URL and token first',
},
{ status: 400 }
);
}
// Fetch all libraries from Plex
const libraries = await plexService.getLibraries(plexUrl, plexToken);
// Filter for audiobook/music libraries (type 8 or 15)
const audioLibraries = libraries.filter((lib: any) =>
lib.type === 'artist' || lib.type === 'music' || lib.title.toLowerCase().includes('audio')
);
return NextResponse.json({
success: true,
libraries: audioLibraries.map((lib: any) => ({
id: lib.key,
title: lib.title,
type: lib.type,
})),
});
} catch (error) {
console.error('[Plex] Failed to fetch libraries:', error);
return NextResponse.json(
{
success: false,
error: 'Failed to fetch Plex libraries',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
});
});
}
+93
View File
@@ -0,0 +1,93 @@
/**
* Component: Admin Plex 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 { getPlexService } from '@/lib/integrations/plex.service';
export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { url, token, libraryId } = await request.json();
if (!url || !token || !libraryId) {
return NextResponse.json(
{ error: 'URL, token, and library ID are required' },
{ status: 400 }
);
}
// Update configuration
await prisma.configuration.upsert({
where: { key: 'plex_url' },
update: { value: url },
create: { key: 'plex_url', value: url },
});
// Only update token if it's not the masked value
if (!token.startsWith('••••')) {
await prisma.configuration.upsert({
where: { key: 'plex_token' },
update: { value: token },
create: { key: 'plex_token', value: token },
});
}
await prisma.configuration.upsert({
where: { key: 'plex_audiobook_library_id' },
update: { value: libraryId },
create: { key: 'plex_audiobook_library_id', value: libraryId },
});
// Fetch and save machine identifier (for server-specific access tokens)
// This is needed for BookDate per-user rating functionality
try {
const plexService = getPlexService();
const actualToken = token.startsWith('••••') ? null : token;
// Get token from DB if it was masked
const tokenToUse = actualToken || (await prisma.configuration.findUnique({
where: { key: 'plex_token' },
}))?.value;
if (tokenToUse) {
const serverInfo = await plexService.testConnection(url, tokenToUse);
if (serverInfo.success && serverInfo.info?.machineIdentifier) {
await prisma.configuration.upsert({
where: { key: 'plex_machine_identifier' },
update: { value: serverInfo.info.machineIdentifier },
create: { key: 'plex_machine_identifier', value: serverInfo.info.machineIdentifier },
});
console.log('[Admin] machineIdentifier updated:', serverInfo.info.machineIdentifier);
} else {
console.warn('[Admin] Could not fetch machineIdentifier');
}
}
} catch (error) {
console.error('[Admin] Error fetching machineIdentifier:', error);
// Don't fail the request if machineIdentifier fetch fails
}
console.log('[Admin] Plex settings updated');
return NextResponse.json({
success: true,
message: 'Plex settings updated successfully',
});
} catch (error) {
console.error('[Admin] Failed to update Plex settings:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to update settings',
},
{ status: 500 }
);
}
});
});
}
@@ -0,0 +1,126 @@
/**
* Component: Prowlarr Indexers API Route
* Documentation: documentation/backend/api.md
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { getProwlarrService } from '@/lib/integrations/prowlarr.service';
import { getConfigService } from '@/lib/services/config.service';
interface SavedIndexerConfig {
id: number;
name: string;
priority: number;
seedingTimeMinutes: number;
rssEnabled?: boolean;
}
/**
* GET /api/admin/settings/prowlarr/indexers
* Fetch available Prowlarr indexers with configuration
*/
export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const prowlarrService = await getProwlarrService();
const configService = getConfigService();
// Fetch indexers from Prowlarr
const indexers = await prowlarrService.getIndexers();
// Get saved indexer configuration (matches wizard format)
const savedConfigStr = await configService.get('prowlarr_indexers');
const savedIndexers: SavedIndexerConfig[] = savedConfigStr ? JSON.parse(savedConfigStr) : [];
// Merge with defaults (wizard format: array of {id, name, priority, seedingTimeMinutes})
const savedIndexersMap = new Map<number, SavedIndexerConfig>(
savedIndexers.map((idx) => [idx.id, idx])
);
const indexersWithConfig = indexers.map((indexer: any) => {
const saved = savedIndexersMap.get(indexer.id);
return {
id: indexer.id,
name: indexer.name,
protocol: indexer.protocol,
privacy: indexer.privacy,
enabled: !!saved, // Enabled if in saved list
priority: saved?.priority || 10,
seedingTimeMinutes: saved?.seedingTimeMinutes ?? 0,
rssEnabled: saved?.rssEnabled ?? false,
supportsRss: indexer.capabilities?.supportsRss !== false, // Default to true if not specified
};
});
return NextResponse.json({
success: true,
indexers: indexersWithConfig,
});
} catch (error) {
console.error('[Prowlarr] Failed to fetch indexers:', error);
return NextResponse.json(
{
success: false,
error: 'Failed to fetch Prowlarr indexers',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
});
});
}
/**
* PUT /api/admin/settings/prowlarr/indexers
* Save indexer configuration
*/
export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { indexers } = await req.json();
// Filter to only enabled indexers and convert to wizard format
const enabledIndexers = indexers
.filter((indexer: any) => indexer.enabled)
.map((indexer: any) => ({
id: indexer.id,
name: indexer.name,
priority: indexer.priority,
seedingTimeMinutes: indexer.seedingTimeMinutes,
rssEnabled: indexer.rssEnabled || false,
}));
// Save to configuration (matches wizard format)
const configService = getConfigService();
await configService.setMany([
{
key: 'prowlarr_indexers',
value: JSON.stringify(enabledIndexers),
category: 'indexer',
description: 'Prowlarr indexer settings (enabled, priority, seeding time)',
},
]);
return NextResponse.json({
success: true,
message: 'Indexer configuration saved',
});
} catch (error) {
console.error('[Prowlarr] Failed to save indexer config:', error);
return NextResponse.json(
{
success: false,
error: 'Failed to save indexer configuration',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
});
});
}
@@ -0,0 +1,57 @@
/**
* Component: Admin Prowlarr 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';
export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { url, apiKey } = await request.json();
if (!url || !apiKey) {
return NextResponse.json(
{ error: 'URL and API key are required' },
{ status: 400 }
);
}
// Update configuration
await prisma.configuration.upsert({
where: { key: 'prowlarr_url' },
update: { value: url },
create: { key: 'prowlarr_url', value: url },
});
// Only update API key if it's not the masked value
if (!apiKey.startsWith('••••')) {
await prisma.configuration.upsert({
where: { key: 'prowlarr_api_key' },
update: { value: apiKey },
create: { key: 'prowlarr_api_key', value: apiKey },
});
}
console.log('[Admin] Prowlarr settings updated');
return NextResponse.json({
success: true,
message: 'Prowlarr settings updated successfully',
});
} catch (error) {
console.error('[Admin] Failed to update Prowlarr settings:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to update settings',
},
{ status: 500 }
);
}
});
});
}
@@ -0,0 +1,37 @@
/**
* Registration Settings API
* Documentation: documentation/features/audiobookshelf-integration.md
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const body = await request.json();
const { enabled, requireAdminApproval } = body;
const { getConfigService } = await import('@/lib/services/config.service');
const configService = getConfigService();
await configService.setMany([
{ key: 'auth.registration_enabled', value: enabled ? 'true' : 'false' },
{ key: 'auth.require_admin_approval', value: requireAdminApproval ? 'true' : 'false' },
]);
return NextResponse.json({
success: true,
message: 'Registration settings saved successfully'
});
} catch (error) {
console.error('[Admin] Failed to save registration settings:', error);
return NextResponse.json(
{ error: 'Failed to save settings' },
{ status: 500 }
);
}
});
});
}
+87
View File
@@ -0,0 +1,87 @@
/**
* Component: Admin 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';
export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
// Fetch all configuration
const configs = await prisma.configuration.findMany();
const configMap = new Map(configs.map((c) => [c.key, c.value]));
// Mask sensitive values
const maskValue = (key: string, value: string | null | undefined) => {
const sensitiveKeys = ['token', 'api_key', 'password'];
if (value && sensitiveKeys.some((k) => key.includes(k))) {
return '••••••••••••';
}
return value || '';
};
// Build response object
const settings = {
backendMode: configMap.get('system.backend_mode') || 'plex',
plex: {
url: configMap.get('plex_url') || '',
token: maskValue('token', configMap.get('plex_token')),
libraryId: configMap.get('plex_audiobook_library_id') || '',
},
audiobookshelf: {
serverUrl: configMap.get('audiobookshelf.server_url') || '',
apiToken: maskValue('api_token', configMap.get('audiobookshelf.api_token')),
libraryId: configMap.get('audiobookshelf.library_id') || '',
},
oidc: {
enabled: configMap.get('oidc.enabled') === 'true',
providerName: configMap.get('oidc.provider_name') || '',
issuerUrl: configMap.get('oidc.issuer_url') || '',
clientId: configMap.get('oidc.client_id') || '',
clientSecret: maskValue('client_secret', configMap.get('oidc.client_secret')),
},
registration: {
enabled: configMap.get('auth.registration_enabled') === 'true',
requireAdminApproval: configMap.get('auth.require_admin_approval') === 'true',
},
prowlarr: {
url: configMap.get('prowlarr_url') || '',
apiKey: maskValue('api_key', configMap.get('prowlarr_api_key')),
},
downloadClient: {
type: configMap.get('download_client_type') || 'qbittorrent',
url: configMap.get('download_client_url') || '',
username: configMap.get('download_client_username') || '',
password: maskValue('password', configMap.get('download_client_password')),
seedingTimeMinutes: parseInt(configMap.get('seeding_time_minutes') || '0'),
},
paths: {
downloadDir: configMap.get('download_dir') || '/downloads',
mediaDir: configMap.get('media_dir') || '/media/audiobooks',
metadataTaggingEnabled: configMap.get('metadata_tagging_enabled') === 'true',
},
general: {
appName: configMap.get('app_name') || 'ReadMeABook',
allowRegistrations: configMap.get('allow_registrations') === 'true',
maxConcurrentDownloads: parseInt(
configMap.get('max_concurrent_downloads') || '3'
),
autoApproveRequests: configMap.get('auto_approve_requests') === 'true',
},
};
return NextResponse.json(settings);
} catch (error) {
console.error('[Admin] Failed to fetch settings:', error);
return NextResponse.json(
{ error: 'Failed to fetch settings' },
{ status: 500 }
);
}
});
});
}
@@ -0,0 +1,71 @@
/**
* Component: Admin Settings Test Download Client 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 { QBittorrentService } from '@/lib/integrations/qbittorrent.service';
export async function POST(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { type, url, username, password } = await request.json();
if (!type || !url || !username || !password) {
return NextResponse.json(
{ success: false, error: 'All fields are required' },
{ status: 400 }
);
}
if (type !== 'qbittorrent') {
return NextResponse.json(
{ success: false, error: 'Only qBittorrent is currently supported' },
{ status: 400 }
);
}
// If password is masked, fetch the actual value from database
let actualPassword = password;
if (password.startsWith('••••')) {
const storedPassword = await prisma.configuration.findUnique({
where: { key: 'download_client_password' },
});
if (!storedPassword?.value) {
return NextResponse.json(
{ success: false, error: 'No stored password found. Please re-enter your download client password.' },
{ status: 400 }
);
}
actualPassword = storedPassword.value;
}
// Test connection with credentials
const version = await QBittorrentService.testConnectionWithCredentials(
url,
username,
actualPassword
);
return NextResponse.json({
success: true,
version,
});
} catch (error) {
console.error('[Admin Settings] Download client test failed:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to connect to download client',
},
{ status: 500 }
);
}
});
});
}
@@ -0,0 +1,84 @@
/**
* Component: Admin Settings Test Plex 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 { getPlexService } from '@/lib/integrations/plex.service';
export async function POST(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { url, token } = await request.json();
if (!url || !token) {
return NextResponse.json(
{ success: false, error: 'URL and token are required' },
{ status: 400 }
);
}
// If token is masked, fetch the actual value from database
let actualToken = token;
if (token.startsWith('••••')) {
const storedToken = await prisma.configuration.findUnique({
where: { key: 'plex_token' },
});
if (!storedToken?.value) {
return NextResponse.json(
{ success: false, error: 'No stored token found. Please re-enter your Plex token.' },
{ status: 400 }
);
}
actualToken = storedToken.value;
}
const plexService = getPlexService();
// Test connection and get server info
const connectionResult = await plexService.testConnection(url, actualToken);
if (!connectionResult.success || !connectionResult.info) {
return NextResponse.json(
{ success: false, error: connectionResult.message },
{ status: 400 }
);
}
// Get libraries
const libraries = await plexService.getLibraries(url, actualToken);
// Format server name safely
const serverName = connectionResult.info
? `${connectionResult.info.platform || 'Plex Server'} v${connectionResult.info.version || 'Unknown'}`
: 'Plex Server';
return NextResponse.json({
success: true,
serverName,
version: connectionResult.info?.version || 'Unknown',
machineIdentifier: connectionResult.info?.machineIdentifier || 'unknown',
libraries: libraries.map((lib) => ({
id: lib.id,
title: lib.title,
type: lib.type,
})),
});
} catch (error) {
console.error('[Admin Settings] Plex test failed:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to connect to Plex',
},
{ status: 500 }
);
}
});
});
}
@@ -0,0 +1,73 @@
/**
* Component: Admin Settings Test Prowlarr 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 { ProwlarrService } from '@/lib/integrations/prowlarr.service';
export async function POST(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { url, apiKey } = await request.json();
if (!url || !apiKey) {
return NextResponse.json(
{ success: false, error: 'URL and API key are required' },
{ status: 400 }
);
}
// If API key is masked, fetch the actual value from database
let actualApiKey = apiKey;
if (apiKey.startsWith('••••')) {
const storedApiKey = await prisma.configuration.findUnique({
where: { key: 'prowlarr_api_key' },
});
if (!storedApiKey?.value) {
return NextResponse.json(
{ success: false, error: 'No stored API key found. Please re-enter your Prowlarr API key.' },
{ status: 400 }
);
}
actualApiKey = storedApiKey.value;
}
// Create a new ProwlarrService instance with test credentials
const prowlarrService = new ProwlarrService(url, actualApiKey);
// Test connection and get indexers
const indexers = await prowlarrService.getIndexers();
// Only return enabled indexers
const enabledIndexers = indexers.filter((indexer) => indexer.enable);
return NextResponse.json({
success: true,
indexerCount: enabledIndexers.length,
totalIndexers: indexers.length,
indexers: enabledIndexers.map((indexer) => ({
id: indexer.id,
name: indexer.name,
protocol: indexer.protocol,
supportsRss: indexer.capabilities?.supportsRss !== false,
})),
});
} catch (error) {
console.error('[Admin Settings] Prowlarr test failed:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to connect to Prowlarr',
},
{ status: 500 }
);
}
});
});
}