Introduces a full notification system with support for Discord and Pushover backends, event triggers, and message formatting. Adds backend services, processors, and API endpoints for managing notifications, as well as a new Notifications tab in the admin settings UI. Updates documentation, database schema, and tests to cover notification features and approval workflow improvements. Also changes project license from MIT to AGPL v3.
11 KiB
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 settingtrue= Always auto-approve for this userfalse= Always require approval for this user
Approval Logic
When user creates request (automatic search via POST /api/requests):
- 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 globalauto_approve_requestssetting- If 'true' → Auto-approve (status: 'pending', send approved notification)
- Otherwise → Require approval (status: 'awaiting_approval', send pending notification)
- If
When user creates request with pre-selected torrent (interactive search):
-
Via POST /api/audiobooks/request-with-torrent (book detail page):
- Check approval requirements (same logic as above)
- If approval needed → Set status to 'awaiting_approval', store torrent in
selectedTorrent, send pending notification - If auto-approved → Set status to 'downloading', start download immediately, send approved notification
-
Via POST /api/requests/{id}/select-torrent (existing request):
- Check if request already in 'awaiting_approval' status → Block with 403 error
- Check approval requirements based on CURRENT settings
- If approval needed → Set status to 'awaiting_approval', store torrent in
selectedTorrent, send pending notification - 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 (clearselectedTorrentfield) - If no
selectedTorrent→ Trigger automatic search job (status: 'pending') - Send approved notification
- If request has
- 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:
{
"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):
{
"success": true,
"request": { /* request with status: 'awaiting_approval' */ },
"message": "Request submitted for admin approval"
}
Response (auto-approved):
{
"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:
{
"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):
{
"success": true,
"request": { /* request with status: 'awaiting_approval' */ },
"message": "Request submitted for admin approval"
}
Response (auto-approved):
{
"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:
{
"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:
{
"action": "approve" | "deny"
}
Approval Logic:
- If request has
selectedTorrent:- Downloads that specific torrent directly (status: 'downloading')
- Clears
selectedTorrentfield 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):
{
"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):
{
"success": true,
"message": "Request approved and search job triggered",
"request": { /* full request object with status: 'pending' */ }
}
Response (deny):
{
"success": true,
"message": "Request denied",
"request": { /* full request object with status: 'denied' */ }
}
Errors:
404- Request not found400- Request not in 'awaiting_approval' status400- Invalid action (must be 'approve' or 'deny')
GET /api/admin/settings/auto-approve
Get global auto-approve setting
Auth: Admin only
Response:
{
"autoApproveRequests": true
}
PATCH /api/admin/settings/auto-approve
Update global auto-approve setting
Auth: Admin only
Request:
{
"autoApproveRequests": true
}
Response:
{
"autoApproveRequests": true
}
PUT /api/admin/users/[id]
Update user (includes autoApproveRequests field)
Auth: Admin only
Request:
{
"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_requestsconfiguration - 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.autoApproveRequestsfield - 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
selectedTorrentfield 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-torrentendpoint 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
autoApproveRequests: Boolean (nullable, default null)
- null: Use global setting
- true: Always auto-approve
- false: Always require approval
Request Table
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
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 - Dashboard UI features
- Database Schema - User and Request tables
- Settings Pages - Global settings management
- BookDate Feature - AI recommendations (Fixed Issues #9)