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:
kikootwo
2026-02-04 14:05:28 -05:00
parent 2ef9ac7be1
commit a0f2ba680d
42 changed files with 1843 additions and 3820 deletions
+13 -6
View File
@@ -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,
});
}
+26 -3
View File
@@ -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,
+28 -5
View File
@@ -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,