mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Add rootless Podman fixes, and others
improve container startup for rootless Podman, plus related refactors and tests. Key changes: - Add/modify Audiobookshelf-related code and wiring (src/lib/services/audiobookshelf/api.ts, library service refs) and update documentation TABLEOFCONTENTS to reference ABS implementation. - Detect user namespace in docker/unified app-start.sh and redis-start.sh and skip gosu when running in rootless Podman to preserve UID mapping; improve startup logging and verification. - Add utility/service files (auth-token-cache.service.ts, credential-migration.service.ts, cleanup-helpers.ts) and corresponding tests; update chapter-merger and metadata-tagger utilities/tests. - Update many admin/auth API routes and tests to reflect changes in settings and integrations. - Remove large AI agent and Audiobookshelf implementation guide docs (AGENTS.md and the implementation guide) and add README note about AI-assisted workflow. These changes enable Audiobookshelf backend mode, improve compatibility with rootless container runtimes, and include cleanup/refactor work and unit tests.
This commit is contained in:
@@ -8,6 +8,7 @@ import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middlewar
|
||||
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');
|
||||
@@ -97,10 +98,15 @@ export async function PUT(
|
||||
}
|
||||
}
|
||||
|
||||
// Update clients array
|
||||
// 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(clients) },
|
||||
{ key: 'download_clients', value: JSON.stringify(encryptedClients) },
|
||||
]);
|
||||
|
||||
// Invalidate cache
|
||||
@@ -153,10 +159,15 @@ export async function DELETE(
|
||||
|
||||
const deletedClient = clients[clientIndex];
|
||||
|
||||
// Remove client from array
|
||||
// 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(updatedClients) },
|
||||
{ key: 'download_clients', value: JSON.stringify(encryptedClients) },
|
||||
]);
|
||||
|
||||
// Invalidate cache
|
||||
|
||||
@@ -8,6 +8,7 @@ import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middlewar
|
||||
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';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
@@ -111,7 +112,7 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Create new client config
|
||||
// Create new client config for testing (with plaintext password)
|
||||
// qBittorrent credentials are optional (supports IP whitelist auth)
|
||||
const newClient: DownloadClientConfig = {
|
||||
id: randomUUID(),
|
||||
@@ -120,7 +121,7 @@ export async function POST(request: NextRequest) {
|
||||
enabled: true,
|
||||
url,
|
||||
username: username || '',
|
||||
password: password || '',
|
||||
password: password || '', // Plaintext for connection test
|
||||
disableSSLVerify: disableSSLVerify || false,
|
||||
remotePathMappingEnabled: remotePathMappingEnabled || false,
|
||||
remotePath: remotePath || undefined,
|
||||
@@ -137,10 +138,17 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Encrypt all passwords before saving (existing clients come decrypted from getAllClients)
|
||||
const encryptionService = getEncryptionService();
|
||||
const allClients = [...existingClients, newClient];
|
||||
const encryptedClients = allClients.map(c => ({
|
||||
...c,
|
||||
password: c.password ? encryptionService.encrypt(c.password) : '',
|
||||
}));
|
||||
|
||||
// Save updated clients array
|
||||
const updatedClients = [...existingClients, newClient];
|
||||
await config.setMany([
|
||||
{ key: 'download_clients', value: JSON.stringify(updatedClients) },
|
||||
{ key: 'download_clients', value: JSON.stringify(encryptedClients) },
|
||||
]);
|
||||
|
||||
// Invalidate cache
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Admin.Settings.Paths');
|
||||
@@ -84,6 +85,14 @@ export async function PUT(request: NextRequest) {
|
||||
|
||||
logger.info('Paths settings updated');
|
||||
|
||||
// Clear config cache for all updated keys so services get fresh values
|
||||
const configService = getConfigService();
|
||||
configService.clearCache('download_dir');
|
||||
configService.clearCache('media_dir');
|
||||
configService.clearCache('audiobook_path_template');
|
||||
configService.clearCache('metadata_tagging_enabled');
|
||||
configService.clearCache('chapter_merging_enabled');
|
||||
|
||||
// Invalidate qBittorrent service singleton to force reload of download_dir
|
||||
const { invalidateQBittorrentService } = await import('@/lib/integrations/qbittorrent.service');
|
||||
invalidateQBittorrentService();
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
import { getEncryptionService } from '@/lib/services/encryption.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.AdminPlexSettings');
|
||||
@@ -33,10 +34,12 @@ export async function PUT(request: NextRequest) {
|
||||
|
||||
// Only update token if it's not the masked value
|
||||
if (!token.startsWith('••••')) {
|
||||
const encryptionService = getEncryptionService();
|
||||
const encryptedToken = encryptionService.encrypt(token);
|
||||
await prisma.configuration.upsert({
|
||||
where: { key: 'plex_token' },
|
||||
update: { value: token },
|
||||
create: { key: 'plex_token', value: token },
|
||||
update: { value: encryptedToken, encrypted: true },
|
||||
create: { key: 'plex_token', value: encryptedToken, encrypted: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,10 +62,10 @@ export async function PUT(request: NextRequest) {
|
||||
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;
|
||||
// Get token from DB if it was masked (decrypted via ConfigService)
|
||||
const { getConfigService } = await import('@/lib/services/config.service');
|
||||
const configService = getConfigService();
|
||||
const tokenToUse = actualToken || await configService.get('plex_token');
|
||||
|
||||
if (tokenToUse) {
|
||||
const serverInfo = await plexService.testConnection(url, tokenToUse);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getEncryptionService } from '@/lib/services/encryption.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Admin.Settings.Prowlarr');
|
||||
@@ -32,10 +33,12 @@ export async function PUT(request: NextRequest) {
|
||||
|
||||
// Only update API key if it's not the masked value
|
||||
if (!apiKey.startsWith('••••')) {
|
||||
const encryptionService = getEncryptionService();
|
||||
const encryptedApiKey = encryptionService.encrypt(apiKey);
|
||||
await prisma.configuration.upsert({
|
||||
where: { key: 'prowlarr_api_key' },
|
||||
update: { value: apiKey },
|
||||
create: { key: 'prowlarr_api_key', value: apiKey },
|
||||
update: { value: encryptedApiKey, encrypted: true },
|
||||
create: { key: 'prowlarr_api_key', value: encryptedApiKey, encrypted: true },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
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 { RMABLogger } from '@/lib/utils/logger';
|
||||
@@ -43,21 +44,24 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// If password is masked, fetch the actual value from database
|
||||
// If password is masked, fetch the actual value from download client manager (decrypted)
|
||||
let actualPassword = password;
|
||||
if (password && password.startsWith('••••')) {
|
||||
const storedPassword = await prisma.configuration.findUnique({
|
||||
where: { key: 'download_client_password' },
|
||||
});
|
||||
if (password && (password.startsWith('••••') || password === '********')) {
|
||||
const configService = getConfigService();
|
||||
const manager = getDownloadClientManager(configService);
|
||||
const clients = await manager.getAllClients();
|
||||
|
||||
if (!storedPassword?.value) {
|
||||
// Find the first client of matching type to get its password
|
||||
const matchingClient = clients.find(c => c.type === type);
|
||||
|
||||
if (!matchingClient?.password) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'No stored password/API key found. Please re-enter it.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
actualPassword = storedPassword.value;
|
||||
actualPassword = matchingClient.password;
|
||||
}
|
||||
|
||||
// Validate required fields per client type and test connection
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { getPlexService } from '@/lib/integrations/plex.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
@@ -24,21 +24,20 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// If token is masked, fetch the actual value from database
|
||||
// If token is masked, fetch the actual value from database (decrypted)
|
||||
let actualToken = token;
|
||||
if (token.startsWith('••••')) {
|
||||
const storedToken = await prisma.configuration.findUnique({
|
||||
where: { key: 'plex_token' },
|
||||
});
|
||||
const configService = getConfigService();
|
||||
const storedToken = await configService.get('plex_token');
|
||||
|
||||
if (!storedToken?.value) {
|
||||
if (!storedToken) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'No stored token found. Please re-enter your Plex token.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
actualToken = storedToken.value;
|
||||
actualToken = storedToken;
|
||||
}
|
||||
|
||||
const plexService = getPlexService();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { ProwlarrService } from '@/lib/integrations/prowlarr.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
@@ -24,21 +24,20 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// If API key is masked, fetch the actual value from database
|
||||
// If API key is masked, fetch the actual value from database (decrypted)
|
||||
let actualApiKey = apiKey;
|
||||
if (apiKey.startsWith('••••')) {
|
||||
const storedApiKey = await prisma.configuration.findUnique({
|
||||
where: { key: 'prowlarr_api_key' },
|
||||
});
|
||||
const configService = getConfigService();
|
||||
const storedApiKey = await configService.get('prowlarr_api_key');
|
||||
|
||||
if (!storedApiKey?.value) {
|
||||
if (!storedApiKey) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'No stored API key found. Please re-enter your Prowlarr API key.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
actualApiKey = storedApiKey.value;
|
||||
actualApiKey = storedApiKey;
|
||||
}
|
||||
|
||||
// Create a new ProwlarrService instance with test credentials
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getPlexService } from '@/lib/integrations/plex.service';
|
||||
import { getEncryptionService } from '@/lib/services/encryption.service';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { getAuthTokenCache } from '@/lib/services/auth-token-cache.service';
|
||||
import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
@@ -149,12 +150,19 @@ export async function GET(request: NextRequest) {
|
||||
if (homeUsers.length > 1) {
|
||||
logger.info('Account has multiple home profiles, redirecting to profile selection');
|
||||
|
||||
// SECURITY: Store the Plex token server-side instead of exposing it to the client
|
||||
// The token is keyed by pinId and will be retrieved during profile selection/switch
|
||||
const tokenCache = getAuthTokenCache();
|
||||
tokenCache.set(pinId, authToken);
|
||||
logger.debug('Plex token cached for profile selection', { pinId });
|
||||
|
||||
// Detect if this is a browser request (mobile redirect) vs AJAX (desktop popup polling)
|
||||
const accept = request.headers.get('accept') || '';
|
||||
const isBrowserRequest = accept.includes('text/html');
|
||||
|
||||
if (isBrowserRequest) {
|
||||
// For browser requests (mobile), construct redirect URL with session data
|
||||
// For browser requests (mobile), construct redirect URL
|
||||
// Token is stored server-side, only pinId is passed to client
|
||||
const host = request.headers.get('host') || 'localhost:3030';
|
||||
const protocol = request.headers.get('x-forwarded-proto') ||
|
||||
(process.env.NODE_ENV === 'production' ? 'https' : 'http');
|
||||
@@ -162,7 +170,8 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
logger.debug('Redirecting to profile selection', { selectProfileUrl });
|
||||
|
||||
// Return HTML page with JavaScript to store token in sessionStorage and redirect
|
||||
// Return HTML page that redirects to profile selection
|
||||
// Note: Token is NOT included - it's stored server-side for security
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -172,9 +181,7 @@ export async function GET(request: NextRequest) {
|
||||
<body>
|
||||
<p>Loading profiles...</p>
|
||||
<script>
|
||||
// Store main account token in session storage for profile selection page
|
||||
sessionStorage.setItem('plex_main_token', '${authToken}');
|
||||
// Redirect to profile selection
|
||||
// Redirect to profile selection (token is stored server-side)
|
||||
window.location.href = '${selectProfileUrl}';
|
||||
</script>
|
||||
</body>
|
||||
@@ -189,12 +196,12 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
} else {
|
||||
// For AJAX requests (desktop popup), return JSON with redirect instruction
|
||||
// Note: Token is NOT included - it's stored server-side for security
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
authorized: true,
|
||||
requiresProfileSelection: true,
|
||||
redirectUrl: `/auth/select-profile?pinId=${pinId}`,
|
||||
mainAccountToken: authToken, // Client will store this temporarily
|
||||
homeUsers: homeUsers.length,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getPlexService } from '@/lib/integrations/plex.service';
|
||||
import { getAuthTokenCache } from '@/lib/services/auth-token-cache.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Auth.Plex.HomeUsers');
|
||||
@@ -12,16 +13,36 @@ const logger = RMABLogger.create('API.Auth.Plex.HomeUsers');
|
||||
/**
|
||||
* GET /api/auth/plex/home-users
|
||||
* Get list of Plex Home profiles for authenticated user
|
||||
*
|
||||
* Authentication: Provide X-Plex-Pin-Id header with the PIN ID from OAuth flow.
|
||||
* The Plex token is retrieved from server-side cache for security.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authToken = request.headers.get('X-Plex-Token');
|
||||
// Get pinId from header - token is stored server-side for security
|
||||
const pinId = request.headers.get('X-Plex-Pin-Id');
|
||||
|
||||
if (!authToken) {
|
||||
if (!pinId) {
|
||||
logger.warn('Missing X-Plex-Pin-Id header');
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Unauthorized',
|
||||
message: 'Missing authentication token',
|
||||
message: 'Missing PIN ID. Please restart the login process.',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Retrieve the Plex token from server-side cache
|
||||
const tokenCache = getAuthTokenCache();
|
||||
const authToken = tokenCache.get(pinId);
|
||||
|
||||
if (!authToken) {
|
||||
logger.warn('Token not found or expired for pinId', { pinId });
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'SessionExpired',
|
||||
message: 'Your session has expired. Please restart the login process.',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
@@ -30,6 +51,8 @@ export async function GET(request: NextRequest) {
|
||||
const plexService = getPlexService();
|
||||
const users = await plexService.getHomeUsers(authToken);
|
||||
|
||||
logger.debug('Home users retrieved', { pinId, userCount: users.length });
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
users,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getPlexService } from '@/lib/integrations/plex.service';
|
||||
import { getEncryptionService } from '@/lib/services/encryption.service';
|
||||
import { getAuthTokenCache } from '@/lib/services/auth-token-cache.service';
|
||||
import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
@@ -15,23 +16,41 @@ const logger = RMABLogger.create('API.PlexSwitchProfile');
|
||||
/**
|
||||
* POST /api/auth/plex/switch-profile
|
||||
* Switch to a Plex Home profile and complete authentication
|
||||
*
|
||||
* Authentication: Provide pinId in request body. The Plex token is
|
||||
* retrieved from server-side cache for security.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const mainAccountToken = request.headers.get('X-Plex-Token');
|
||||
const body = await request.json();
|
||||
const { userId, pin, pinId, profileInfo } = body;
|
||||
|
||||
if (!mainAccountToken) {
|
||||
// Validate pinId is provided
|
||||
if (!pinId) {
|
||||
logger.warn('Missing pinId in request body');
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Unauthorized',
|
||||
message: 'Missing authentication token',
|
||||
message: 'Missing PIN ID. Please restart the login process.',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { userId, pin, pinId, profileInfo } = body;
|
||||
// Retrieve the Plex token from server-side cache
|
||||
const tokenCache = getAuthTokenCache();
|
||||
const mainAccountToken = tokenCache.get(pinId);
|
||||
|
||||
if (!mainAccountToken) {
|
||||
logger.warn('Token not found or expired for pinId', { pinId });
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'SessionExpired',
|
||||
message: 'Your session has expired. Please restart the login process.',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
@@ -155,6 +174,10 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const refreshToken = generateRefreshToken(user.id);
|
||||
|
||||
// Clean up the cached Plex token - authentication is complete
|
||||
tokenCache.delete(pinId);
|
||||
logger.debug('Cached Plex token cleaned up after successful auth', { pinId });
|
||||
|
||||
// Return tokens and user info
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getSchedulerService } from '@/lib/services/scheduler.service';
|
||||
import { runCredentialMigration } from '@/lib/services/credential-migration.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Init');
|
||||
@@ -18,6 +19,9 @@ export async function GET(request: NextRequest) {
|
||||
try {
|
||||
logger.info('Initializing application services...');
|
||||
|
||||
// Run credential migration (encrypts any plaintext credentials)
|
||||
await runCredentialMigration();
|
||||
|
||||
// Initialize scheduler service
|
||||
const schedulerService = getSchedulerService();
|
||||
await schedulerService.start();
|
||||
|
||||
@@ -193,10 +193,11 @@ export async function POST(request: NextRequest) {
|
||||
create: { key: 'plex_url', value: plex.url },
|
||||
});
|
||||
|
||||
const encryptedPlexToken = encryptionService.encrypt(plex.token);
|
||||
await prisma.configuration.upsert({
|
||||
where: { key: 'plex_token' },
|
||||
update: { value: plex.token },
|
||||
create: { key: 'plex_token', value: plex.token },
|
||||
update: { value: encryptedPlexToken, encrypted: true },
|
||||
create: { key: 'plex_token', value: encryptedPlexToken, encrypted: true },
|
||||
});
|
||||
|
||||
await prisma.configuration.upsert({
|
||||
@@ -375,10 +376,11 @@ export async function POST(request: NextRequest) {
|
||||
create: { key: 'prowlarr_url', value: prowlarr.url },
|
||||
});
|
||||
|
||||
const encryptedProwlarrApiKey = encryptionService.encrypt(prowlarr.api_key);
|
||||
await prisma.configuration.upsert({
|
||||
where: { key: 'prowlarr_api_key' },
|
||||
update: { value: prowlarr.api_key },
|
||||
create: { key: 'prowlarr_api_key', value: prowlarr.api_key },
|
||||
update: { value: encryptedProwlarrApiKey, encrypted: true },
|
||||
create: { key: 'prowlarr_api_key', value: encryptedProwlarrApiKey, encrypted: true },
|
||||
});
|
||||
|
||||
await prisma.configuration.upsert({
|
||||
@@ -414,10 +416,16 @@ export async function POST(request: NextRequest) {
|
||||
throw new Error('Invalid download client configuration');
|
||||
}
|
||||
|
||||
// Encrypt passwords in download clients
|
||||
const encryptedClients = downloadClientsArray.map(client => ({
|
||||
...client,
|
||||
password: client.password ? encryptionService.encrypt(client.password) : '',
|
||||
}));
|
||||
|
||||
await prisma.configuration.upsert({
|
||||
where: { key: 'download_clients' },
|
||||
update: { value: JSON.stringify(downloadClientsArray) },
|
||||
create: { key: 'download_clients', value: JSON.stringify(downloadClientsArray) },
|
||||
update: { value: JSON.stringify(encryptedClients) },
|
||||
create: { key: 'download_clients', value: JSON.stringify(encryptedClients) },
|
||||
});
|
||||
|
||||
// Path configuration
|
||||
|
||||
Reference in New Issue
Block a user