Add admin request deletion with soft delete and cleanup

Implements admin ability to delete requests with soft delete, media file cleanup, and seeding-aware torrent management. Adds new API endpoint, frontend confirmation dialog, and request actions dropdown. Updates database schema with deletedAt and deletedBy fields, and ensures all queries filter out deleted requests. Documentation added for feature and user flow.
This commit is contained in:
kikootwo
2025-12-22 20:24:43 -05:00
parent bba4af7398
commit 174e9f05b6
26 changed files with 1936 additions and 200 deletions
+21 -9
View File
@@ -26,8 +26,11 @@ export async function GET(
const { id } = await params;
const requestRecord = await prisma.request.findUnique({
where: { id },
const requestRecord = await prisma.request.findFirst({
where: {
id,
deletedAt: null, // Only show active requests
},
include: {
audiobook: true,
user: {
@@ -100,13 +103,16 @@ export async function PATCH(
const body = await req.json();
const { action } = body;
const requestRecord = await prisma.request.findUnique({
where: { id },
const requestRecord = await prisma.request.findFirst({
where: {
id,
deletedAt: null, // Only allow updates to active requests
},
});
if (!requestRecord) {
return NextResponse.json(
{ error: 'NotFound', message: 'Request not found' },
{ error: 'NotFound', message: 'Request not found or already deleted' },
{ status: 404 }
);
}
@@ -161,8 +167,11 @@ export async function PATCH(
if (requestRecord.status === 'warn' || requestRecord.status === 'awaiting_import') {
// Retry import
const requestWithData = await prisma.request.findUnique({
where: { id },
const requestWithData = await prisma.request.findFirst({
where: {
id,
deletedAt: null,
},
include: {
audiobook: true,
downloadHistory: {
@@ -213,8 +222,11 @@ export async function PATCH(
jobType = 'import';
} else {
// Retry search
const requestWithData = await prisma.request.findUnique({
where: { id },
const requestWithData = await prisma.request.findFirst({
where: {
id,
deletedAt: null,
},
include: {
audiobook: true,
},
+21 -15
View File
@@ -80,13 +80,12 @@ export async function POST(request: NextRequest) {
});
}
// Check if user already has a request for this audiobook
const existingRequest = await prisma.request.findUnique({
// Check if user already has an active (non-deleted) request for this audiobook
const existingRequest = await prisma.request.findFirst({
where: {
userId_audiobookId: {
userId: req.user.id,
audiobookId: audiobookRecord.id,
},
userId: req.user.id,
audiobookId: audiobookRecord.id,
deletedAt: null, // Only check active requests
},
});
@@ -112,12 +111,15 @@ export async function POST(request: NextRequest) {
});
}
// Create request
// Check if we should skip auto-search (for interactive search)
const skipAutoSearch = req.nextUrl.searchParams.get('skipAutoSearch') === 'true';
// Create request with appropriate status
const newRequest = await prisma.request.create({
data: {
userId: req.user.id,
audiobookId: audiobookRecord.id,
status: 'pending',
status: skipAutoSearch ? 'awaiting_search' : 'pending',
progress: 0,
},
include: {
@@ -131,13 +133,15 @@ export async function POST(request: NextRequest) {
},
});
// Trigger search job
const jobQueue = getJobQueueService();
await jobQueue.addSearchJob(newRequest.id, {
id: audiobookRecord.id,
title: audiobookRecord.title,
author: audiobookRecord.author,
});
// Trigger search job only if not skipped
if (!skipAutoSearch) {
const jobQueue = getJobQueueService();
await jobQueue.addSearchJob(newRequest.id, {
id: audiobookRecord.id,
title: audiobookRecord.title,
author: audiobookRecord.author,
});
}
return NextResponse.json({
success: true,
@@ -194,6 +198,8 @@ export async function GET(request: NextRequest) {
if (status) {
where.status = status;
}
// Only show active (non-deleted) requests
where.deletedAt = null;
const requests = await prisma.request.findMany({
where,