Files
ReadMeABook/src/app/api/admin/users/[id]/route.ts
T
kikootwo 4b90b35748 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.
2026-02-09 19:45:43 -05:00

244 lines
8.1 KiB
TypeScript

/**
* Component: Admin User Update API
* Documentation: documentation/admin-dashboard.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.Users');
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { id } = await params;
const body = await request.json();
const { role, autoApproveRequests, interactiveSearchAccess } = body;
// Validate role
if (!role || (role !== 'user' && role !== 'admin')) {
return NextResponse.json(
{ error: 'Invalid role. Must be "user" or "admin"' },
{ status: 400 }
);
}
// Validate autoApproveRequests (optional)
if (autoApproveRequests !== undefined && autoApproveRequests !== null && typeof autoApproveRequests !== 'boolean') {
return NextResponse.json(
{ error: 'Invalid autoApproveRequests. Must be a boolean or null' },
{ status: 400 }
);
}
// 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(
{ error: 'You cannot change your own role' },
{ status: 403 }
);
}
// Check if user is the setup admin, OIDC user, or deleted
const targetUser = await prisma.user.findUnique({
where: { id },
select: {
isSetupAdmin: true,
authProvider: true,
plexUsername: true,
deletedAt: true,
role: true, // Need current role to detect role changes
},
});
if (!targetUser) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
// Prevent changing deleted users
if (targetUser.deletedAt) {
return NextResponse.json(
{ error: 'Cannot modify a deleted user' },
{ status: 403 }
);
}
// Detect if role is being changed
const isRoleChange = targetUser.role !== role;
// Prevent changing setup admin role (only if role is actually being changed)
if (targetUser.isSetupAdmin && isRoleChange && role !== 'admin') {
return NextResponse.json(
{ error: 'Cannot change the setup admin role. This account must always remain an admin.' },
{ status: 403 }
);
}
// Prevent changing OIDC user roles (only if role is actually being changed)
if (targetUser.authProvider === 'oidc' && isRoleChange) {
return NextResponse.json(
{ error: 'Cannot change OIDC user roles. Use admin role mapping in OIDC settings instead.' },
{ status: 403 }
);
}
// 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; interactiveSearchAccess?: boolean | null } = { role };
if (autoApproveRequests !== undefined) {
updateData.autoApproveRequests = autoApproveRequests;
}
if (interactiveSearchAccess !== undefined) {
updateData.interactiveSearchAccess = interactiveSearchAccess;
}
// Update user
const updatedUser = await prisma.user.update({
where: { id },
data: updateData,
select: {
id: true,
plexUsername: true,
role: true,
autoApproveRequests: true,
interactiveSearchAccess: true,
},
});
return NextResponse.json({ user: updatedUser });
} catch (error) {
logger.error('Failed to update user', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: 'Failed to update user' },
{ status: 500 }
);
}
});
});
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { id } = await params;
// Prevent user from deleting themselves
if (req.user && id === req.user.sub) {
return NextResponse.json(
{ error: 'You cannot delete your own account' },
{ status: 403 }
);
}
// Check if user exists and get their details
const targetUser = await prisma.user.findUnique({
where: { id },
select: {
id: true,
plexUsername: true,
isSetupAdmin: true,
authProvider: true,
deletedAt: true,
_count: {
select: { requests: true },
},
},
});
if (!targetUser) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
// Check if user is already deleted
if (targetUser.deletedAt) {
return NextResponse.json(
{ error: 'User has already been deleted' },
{ status: 400 }
);
}
// Prevent deleting setup admin
if (targetUser.isSetupAdmin) {
return NextResponse.json(
{ error: 'Cannot delete the setup admin account. This account is protected.' },
{ status: 403 }
);
}
// Only allow deleting local users (manual registration)
if (targetUser.authProvider !== 'local') {
const providerName = targetUser.authProvider === 'plex' ? 'Plex' :
targetUser.authProvider === 'oidc' ? 'OIDC' :
targetUser.authProvider || 'external';
return NextResponse.json(
{
error: `Cannot delete ${providerName} users. User access is managed by ${providerName}.`
},
{ status: 403 }
);
}
// Soft-delete user (preserves their requests and history)
// Append timestamp to plexId to free it up for reuse (allows username reuse)
const timestamp = Date.now();
await prisma.user.update({
where: { id },
data: {
deletedAt: new Date(),
deletedBy: req.user?.sub || null,
plexId: `local-${targetUser.plexUsername}-deleted-${timestamp}`,
},
});
return NextResponse.json({
success: true,
message: `User "${targetUser.plexUsername}" has been deleted. Their ${targetUser._count.requests} request(s) have been preserved.`
});
} catch (error) {
logger.error('Failed to delete user', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: 'Failed to delete user' },
{ status: 500 }
);
}
});
});
}