mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
4b90b35748
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.
244 lines
8.1 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|