# Request Approval System **Status:** ✅ Implemented | Admin approval workflow for user requests with global & per-user auto-approve controls ## Overview Allows admins to review and approve/deny user requests before they are processed. Supports global auto-approve toggle and per-user auto-approve overrides. Interactive search requests store pre-selected torrents when approval is required. ## Key Details ### Request Statuses - **awaiting_approval** - New status for requests pending admin approval - **denied** - New status for requests rejected by admin - **pending** - Status after approval (triggers search job) - Applies to all existing statuses: pending, searching, downloading, processing, downloaded, available, failed, cancelled, awaiting_search, awaiting_import, warn ### Configuration Keys - `auto_approve_requests` (Configuration table) - Global setting (true/false string) - `User.autoApproveRequests` (User table) - Per-user override (boolean, nullable) - `null` = Use global setting - `true` = Always auto-approve for this user - `false` = Always require approval for this user ### Approval Logic **When user creates request (automatic search via POST /api/requests):** 1. Check `User.autoApproveRequests`: - If `true` → Set status to 'pending', trigger search job, send approved notification - If `false` → Set status to 'awaiting_approval', wait for admin, send pending notification - If `null` → Check global `auto_approve_requests` setting - If 'true' → Auto-approve (status: 'pending', send approved notification) - Otherwise → Require approval (status: 'awaiting_approval', send pending notification) **When user creates request with pre-selected torrent (interactive search):** - **Via POST /api/audiobooks/request-with-torrent** (book detail page): 1. Check approval requirements (same logic as above) 2. If approval needed → Set status to 'awaiting_approval', store torrent in `selectedTorrent`, send pending notification 3. If auto-approved → Set status to 'downloading', start download immediately, send approved notification - **Via POST /api/requests/{id}/select-torrent** (existing request): 1. Check if request already in 'awaiting_approval' status → Block with 403 error 2. Check approval requirements based on CURRENT settings 3. If approval needed → Set status to 'awaiting_approval', store torrent in `selectedTorrent`, send pending notification 4. If auto-approved → Set status to 'downloading', start download immediately, send approved notification **Admin approval actions:** - **Approve:** - If request has `selectedTorrent` → Download that specific torrent (clear `selectedTorrent` field) - If no `selectedTorrent` → Trigger automatic search job (status: 'pending') - Send approved notification - **Deny:** → Change status to 'denied', no further processing ## API Endpoints ### POST /api/audiobooks/request-with-torrent Create request with pre-selected torrent (book detail page interactive search) **Auth:** User or Admin **Request:** ```json { "audiobook": { /* audiobook metadata */ }, "torrent": { /* selected torrent data */ } } ``` **Approval Check:** - Checks approval requirements - If needed → Status 'awaiting_approval', stores torrent, sends pending notification - If auto-approved → Status 'downloading', starts download, sends approved notification **Response (awaiting approval):** ```json { "success": true, "request": { /* request with status: 'awaiting_approval' */ }, "message": "Request submitted for admin approval" } ``` **Response (auto-approved):** ```json { "success": true, "request": { /* request with status: 'downloading' */ } } ``` ### POST /api/requests/[id]/select-torrent Select torrent for existing request (request page interactive search) **Auth:** User (owner) or Admin **Request:** ```json { "torrent": { /* selected torrent data */ } } ``` **Approval Check:** - Blocks if already in 'awaiting_approval' status - Re-checks approval requirements based on CURRENT settings - If needed → Status 'awaiting_approval', stores torrent, sends pending notification - If auto-approved → Status 'downloading', starts download, sends approved notification **Response (awaiting approval):** ```json { "success": true, "request": { /* request with status: 'awaiting_approval' */ }, "message": "Request submitted for admin approval" } ``` **Response (auto-approved):** ```json { "success": true, "request": { /* request with status: 'downloading' */ }, "message": "Torrent download initiated" } ``` ### GET /api/admin/requests/pending-approval Fetch all requests with status 'awaiting_approval' **Auth:** Admin only **Response:** ```json { "success": true, "requests": [ { "id": "uuid", "createdAt": "2026-01-15T12:00:00Z", "audiobook": { "title": "Book Title", "author": "Author Name", "coverArtUrl": "https://..." }, "user": { "id": "uuid", "plexUsername": "username", "avatarUrl": "https://..." } } ], "count": 5 } ``` ### POST /api/admin/requests/[id]/approve Approve or deny a specific request **Auth:** Admin only **Request:** ```json { "action": "approve" | "deny" } ``` **Approval Logic:** - If request has `selectedTorrent`: - Downloads that specific torrent directly (status: 'downloading') - Clears `selectedTorrent` field after use - Message: "Request approved and download started with pre-selected torrent" - If no `selectedTorrent`: - Triggers automatic search job (status: 'pending') - Message: "Request approved and search job triggered" - Both send approved notification **Response (approve with pre-selected torrent):** ```json { "success": true, "message": "Request approved and download started with pre-selected torrent", "request": { /* full request object with status: 'downloading' */ } } ``` **Response (approve without pre-selected torrent):** ```json { "success": true, "message": "Request approved and search job triggered", "request": { /* full request object with status: 'pending' */ } } ``` **Response (deny):** ```json { "success": true, "message": "Request denied", "request": { /* full request object with status: 'denied' */ } } ``` **Errors:** - `404` - Request not found - `400` - Request not in 'awaiting_approval' status - `400` - Invalid action (must be 'approve' or 'deny') ### GET /api/admin/settings/auto-approve Get global auto-approve setting **Auth:** Admin only **Response:** ```json { "autoApproveRequests": true } ``` ### PATCH /api/admin/settings/auto-approve Update global auto-approve setting **Auth:** Admin only **Request:** ```json { "autoApproveRequests": true } ``` **Response:** ```json { "autoApproveRequests": true } ``` ### PUT /api/admin/users/[id] Update user (includes autoApproveRequests field) **Auth:** Admin only **Request:** ```json { "autoApproveRequests": true | false | null } ``` ## UI Features ### Admin Dashboard (/admin) **Requests Awaiting Approval Section:** - Shows only when pending approval requests exist - Grid layout with book cards (3 columns on desktop) - Each card displays: - Book cover image - Title and author - User avatar and username - Request timestamp (relative: "2 hours ago") - Approve button (green, checkmark icon) - Deny button (red, X icon) - Auto-refreshes every 10 seconds (SWR) - Loading states on buttons during approval/denial - Success/error toast notifications - Mutates multiple caches on action: pending-approval, recent requests, metrics ### Admin Users Page (/admin/users) **Global Auto-Approve Toggle:** - Checkbox at top of page - Label: "Auto-approve all requests by default" - Updates `auto_approve_requests` configuration - Optimistic UI update with revert on error - Toast notification on success/error **Per-User Auto-Approve Control:** - Each user row has toggle dropdown: - "Use Global Setting" (null, default) - "Always Auto-Approve" (true) - "Always Require Approval" (false) - Updates `User.autoApproveRequests` field - Shows current effective setting (considers global + per-user) - Optimistic UI update ### User Request Flow **When creating request (POST /api/requests):** - System checks approval logic (see above) - If awaiting approval → User sees status "Awaiting Approval" on request card - If auto-approved → User sees status "Pending" and processing begins ### Request Status Badges - **awaiting_approval** → Amber badge with warning icon - **denied** → Red badge with X icon - All other statuses → Existing badge colors ## Security **Interactive Search Approval Enforcement:** - All interactive search flows (request-with-torrent, select-torrent) check approval requirements - If approval needed, torrent is stored in `selectedTorrent` field and request enters 'awaiting_approval' status - Admin sees exact torrent user selected when reviewing approval - Upon approval, admin approves THAT specific torrent (no re-search) **Settings Change Protection:** - `select-torrent` endpoint re-checks approval requirements based on CURRENT settings - Prevents bypass: User with auto-approve enabled creates request → Admin disables auto-approve → User tries to download - If settings changed, torrent is stored and request enters approval queue **Notification Timing:** - Automatic search: Notification sent immediately on request creation - Interactive search (auto-approved): Notification sent when torrent selected and download starts - Interactive search (approval needed): Pending notification sent immediately, approved notification sent on admin approval ## Database Schema ### User Table ```prisma autoApproveRequests: Boolean (nullable, default null) - null: Use global setting - true: Always auto-approve - false: Always require approval ``` ### Request Table ```prisma status: Enum (includes 'awaiting_approval', 'denied') selectedTorrent: Json (nullable) - Stores pre-selected torrent data from interactive search - Set when approval needed, cleared after admin approval - Contains: guid, title, size, seeders, indexer, downloadUrl, format, etc. ``` ### Configuration Table ```prisma key: 'auto_approve_requests' value: 'true' | 'false' (string) ``` ## Fixed Issues ✅ **1. BookDate Requests Bypass Approval System** - Issue: Requests created through BookDate (right swipe) bypassed approval system entirely - Security Impact: Critical - allowed users to bypass admin approval controls - Cause: BookDate swipe route created requests with hardcoded 'pending' status, no approval checks, no notifications - Fix: Implemented full approval logic in BookDate swipe route (same as POST /api/requests) - Checks user.autoApproveRequests and global auto_approve_requests setting - Sets correct status ('awaiting_approval' or 'pending') - Sends appropriate notifications (request_pending_approval or request_approved) - Only triggers search job if auto-approved - Files updated: `src/app/api/bookdate/swipe/route.ts:124-217`, `tests/api/bookdate.routes.test.ts:470-648` ## Related - [Admin Dashboard](../admin-dashboard.md) - Dashboard UI features - [Database Schema](../backend/database.md) - User and Request tables - [Settings Pages](../settings-pages.md) - Global settings management - [BookDate Feature](../features/bookdate.md) - AI recommendations (Fixed Issues #9)