Admin Video Gallery
Unified paginated surface for browsing, managing, and reporting storage on all video assets
Admin Video Gallery
Admins can browse all video assets (course + training), filter by status or type, delete orphaned assets with automatic S3 cleanup, reassign assets to different lessons or sessions, and view storage usage totals.
All endpoints require a valid admin JWT (Authorization: Bearer <token>).
Endpoints
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /admin/video-gallery | Courses_READ | Paginated asset list |
| GET | /admin/video-gallery/storage-breakdown | Courses_READ | Per-entity storage totals |
| POST | /admin/video-gallery/:id/delete | Courses_DELETE or Training_SESSION_DELETE | Soft-delete + async S3 cleanup |
| PATCH | /admin/video-gallery/:id/reassign | Courses_READ | Move asset to a different entity |
GET /admin/video-gallery
Returns a paginated list of all video assets with per-asset size, status, linked entity, and a totalStorageBytes header across all non-deleted assets.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
size | integer | 20 | Items per page (max 100) |
sort | string | createdAt | Sort field: createdAt, fileSize, originalFilename, status |
order | asc | desc | desc | Sort direction |
search | string | — | Substring match on originalFilename (SQL ILIKE) |
status | string | — | Filter: uploading, processing, ready, failed |
linkedType | string | — | Filter: course, training, orphan |
includeDeleted | boolean | false | Include soft-deleted assets (admin recovery view) |
Response
{
"message": "Video assets retrieved",
"data": {
"items": [
{
"id": "01961a2f-...",
"originalFilename": "intro-lesson.mp4",
"status": "ready",
"fileSize": 524288000,
"duration": 312,
"thumbnailPath": "thumbs/01961a2f-.../thumb.jpg",
"linkedType": "course",
"linkedEntityName": "React Fundamentals",
"linkedLessonId": "01961a2e-...",
"linkedSessionId": null,
"createdAt": "2026-04-21T10:00:00.000Z",
"deletedAt": null
}
],
"totalStorageBytes": 10737418240
},
"count": 47,
"currentPage": 1,
"totalPage": 3
}totalStorageBytes is the sum of fileSize across all non-deleted assets — not just the current page. Frontend can format bytes to human-readable (e.g. "10 GB").
GET /admin/video-gallery/storage-breakdown
Returns aggregated storage grouped by course and training program.
Response
{
"message": "Storage breakdown retrieved",
"data": {
"courses": [
{ "id": 1, "title": "React Fundamentals", "totalBytes": 3221225472, "assetCount": 6 }
],
"trainings": [
{ "id": 42, "title": "Advanced Python Bootcamp", "totalBytes": 2147483648, "assetCount": 4 }
],
"totalBytes": 10737418240
}
}POST /admin/video-gallery/:id/delete
Soft-deletes the video asset row and enqueues an async BullMQ job to remove S3/RustFS objects.
Permission: Courses_DELETE for course-linked assets; Training_SESSION_DELETE for training-linked assets. The endpoint accepts either permission.
What happens
The API response returns immediately after the DB soft-delete. S3 cleanup is best-effort — if individual file deletes fail, the error is logged and the job completes (no BullMQ retry storm).
Response
{
"message": "Video asset deleted and S3 cleanup enqueued",
"data": {
"id": "01961a2f-...",
"deletedAt": "2026-04-21T12:00:00.000Z",
"cascaded": { "lessons": 1, "trainingSessions": 0 }
}
}PATCH /admin/video-gallery/:id/reassign
Moves the video asset reference from its current entity to a new lesson or training session in a single transaction.
Request Body
{
"targetType": "lesson",
"targetId": "01961b00-..."
}| Field | Type | Values | Description |
|---|---|---|---|
targetType | string | lesson, training_session | Type of entity to assign to |
targetId | string | UUID (lesson) or integer string (training_session) | ID of target entity |
Validation rules:
- Asset must be
readystatus - Asset must not be soft-deleted
- Target entity must exist
- Target entity must not already have a different video asset assigned
Response
{
"message": "Video asset reassigned",
"data": {
"videoAssetId": "01961a2f-...",
"targetType": "lesson",
"targetId": "01961b00-..."
}
}Error Codes
| HTTP | Code | Trigger |
|---|---|---|
| 404 | VIDEO_ASSET_NOT_FOUND | No video_asset row matches the given UUID |
| 400 | VIDEO_ASSET_ALREADY_DELETED | Asset deletedAt is already set |
| 400 | VIDEO_ASSET_NOT_READY | Reassign attempted on non-ready asset |
| 404 | VIDEO_ASSET_REASSIGN_TARGET_NOT_FOUND | Target lesson or training session does not exist |
| 400 | VIDEO_ASSET_REASSIGN_TARGET_OCCUPIED | Target entity already has a different asset assigned |
| 400 | VIDEO_ASSET_REASSIGN_INVALID_TYPE | targetType is not lesson or training_session, or targetId is not a valid integer for training_session |
All error responses follow the ResponseDto envelope:
{
"message": "Video asset must be in ready status to reassign.",
"errorCode": "VIDEO_ASSET_NOT_READY"
}Requirements Coverage
| Requirement | Endpoint(s) |
|---|---|
| GAL-01 | GET /admin/video-gallery |
| GAL-02 | GET /admin/video-gallery (status, linkedType, search filters) |
| GAL-03 | POST /admin/video-gallery/:id/delete |
| GAL-04 | PATCH /admin/video-gallery/:id/reassign |
| GAL-05 | GET /admin/video-gallery (totalStorageBytes + per-asset fileSize) |
| GAL-06 | GET /admin/video-gallery/storage-breakdown |