# Assets Controller Documentation **File:** `controllers/admin/assets.controller.js` **Base URL:** `/api/admin/assets` **Guards:** `authenticate → requireAdmin() → adminLimiter` --- ## Table of Contents - [Get All Assets](#get-all-assets) - [Get Single Asset](#get-single-asset) - [Upload Asset](#upload-asset) - [Update Asset Metadata](#update-asset-metadata) - [Update Thumbnail](#update-thumbnail) - [Delete Asset](#delete-asset) - [Bulk Delete Assets](#bulk-delete-assets) - [Restore Asset](#restore-asset) --- ## Get All Assets **`GET /api/admin/assets`** Returns a paginated list of non-deleted assets with optional filtering. ### Query Parameters | Parameter | Type | Required | Description | |-------------|---------|----------|--------------------------------------------------| | page | number | No | Page number. Default: `1` | | limit | number | No | Records per page. Default: `20` | | file_type | string | No | Filter by type: `image`, `video`, `document`, `other` | | owner_type | string | No | Filter by owner type e.g. `User`, `Course` | | owner_id | number | No | Filter by owner ID | | uploadedBy | number | No | Filter by uploader user ID | | is_public | boolean | No | Filter by visibility: `true` or `false` | | resolution | string | No | Filter by resolution e.g. `1080p`, `720p` | | search | string | No | Search by `display_name`, `original_name`, `description` | | sort_by | string | No | Column to sort by. Default: `createdAt` | | sort_dir | string | No | Sort direction: `ASC` or `DESC`. Default: `DESC` | ### Response `200` ```json { "status": "success", "message": "Assets retrieved.", "data": { "rows": [...], "pagination": { "total": 100, "page": 1, "limit": 20, "totalPages": 5 } } } ``` --- ## Get Single Asset **`GET /api/admin/assets/:assetId`** Returns a single non-deleted asset by ID. ### Path Parameters | Parameter | Type | Required | Description | |-----------|--------|----------|--------------| | assetId | number | Yes | Asset ID | ### Response `200` ```json { "status": "success", "message": "Asset found.", "data": { "asset_id": 1, "uuid": "550e8400-e29b-41d4-a716-446655440000", "original_name": "intro.mp4", "display_name": "Course Intro Video", "file_url": "/uploads/intro.mp4", "file_size": 104857600, "mime_type": "video/mp4", "extension": "mp4", "checksum": "a3f5...", "file_type": "video", "width": 1920, "height": 1080, "duration": 120.5, "resolution": "1080p", "frame_rate": 29.97, "bitrate": 8000000, "video_codec": "H.264", "audio_codec": "AAC", "thumbnail_url": "/uploads/thumbnails/intro.jpg", "description": "Introduction to the course.", "storage_provider": "local", "storage_bucket": null, "storage_key": "intro.mp4", "is_public": true, "access_level": "public", "owner_type": "Course", "owner_id": 3, "uploadedBy": 1, "deletedBy": null, "createdAt": "2025-01-01T00:00:00.000Z", "updatedAt": "2025-01-01T00:00:00.000Z", "deletedAt": null } } ``` ### Response `404` ```json { "status": "error", "message": "Asset not found." } ``` --- ## Upload Asset **`POST /api/admin/assets/upload`** Uploads a new asset. Expects `multipart/form-data`. Video metadata (`width`, `height`, `duration`, etc.) should be extracted via **ffprobe** server-side or passed from the client. `resolution` is **auto-derived** from `width` and `height` — do not pass it manually. ### Request `multipart/form-data` | Field | Type | Required | Description | |-----------------|---------|----------|----------------------------------------------------------| | file | File | Yes | The file to upload | | uploadedBy | number | Yes | User ID of the uploader | | display_name | string | No | Display name shown on platform. Defaults to filename | | description | string | No | Description of the asset | | owner_type | string | No | Owning entity type e.g. `Course`, `User` | | owner_id | number | No | Owning entity ID | | is_public | boolean | No | Whether asset is publicly accessible. Default: `false` | | access_level | string | No | `public`, `private`, `restricted`. Default: `private` | | storage_provider| string | No | `local`, `s3`, `gcs`, `cloudinary`, `chibisafe`, `other`. Default: `local` | | storage_bucket | string | No | Bucket/container name for cloud storage | | storage_key | string | No | Object key/path in bucket | | file_url | string | No* | Required for non-local storage providers | | width | number | No | Video/image width in px | | height | number | No | Video/image height in px | | duration | number | No | Video duration in seconds | | frame_rate | number | No | Video frame rate in fps | | bitrate | number | No | Video bitrate in bps | | video_codec | string | No | Video codec e.g. `H.264`, `H.265` | | audio_codec | string | No | Audio codec e.g. `AAC`, `MP3` | | thumbnail_url | string | No | URL of the video/document preview thumbnail | ### Resolution Auto-Derivation | Height (px) | Derived Resolution | |-------------|-------------------| | ≥ 2160 | `4K` | | ≥ 1440 | `1440p` | | ≥ 1080 | `1080p` | | ≥ 720 | `720p` | | ≥ 480 | `480p` | | ≥ 360 | `360p` | | ≥ 240 | `240p` | | Other | `{width}x{height}`| ### Response `201` ```json { "status": "success", "message": "Asset uploaded.", "data": { ...asset } } ``` ### Response `400` ```json { "status": "error", "message": "No file uploaded." } ``` --- ## Update Asset Metadata **`PUT /api/admin/assets/:assetId`** Updates metadata of an existing asset. File replacement is not supported — upload a new asset instead. `resolution` is **auto-re-derived** if `width` or `height` is updated. ### Path Parameters | Parameter | Type | Required | Description | |-----------|--------|----------|-------------| | assetId | number | Yes | Asset ID | ### Request Body `application/json` | Field | Type | Required | Description | |--------------|---------|----------|------------------------------------------| | display_name | string | No | Updated display name | | description | string | No | Updated description | | owner_type | string | No | Updated owner type | | owner_id | number | No | Updated owner ID | | is_public | boolean | No | Updated visibility | | access_level | string | No | Updated access level | | thumbnail_url | string | No | Updated thumbnail URL | | width | number | No | Updated width — re-derives resolution | | height | number | No | Updated height — re-derives resolution | | duration | number | No | Updated duration | | frame_rate | number | No | Updated frame rate | | bitrate | number | No | Updated bitrate | | video_codec | string | No | Updated video codec | | audio_codec | string | No | Updated audio codec | ### Response `200` ```json { "status": "success", "message": "Asset updated.", "data": { ...asset } } ``` --- ## Update Thumbnail **`PATCH /api/admin/assets/:assetId/thumbnail`** Updates only the thumbnail of an asset. Useful for video platforms where users frequently change the video cover independently. ### Path Parameters | Parameter | Type | Required | Description | |-----------|--------|----------|-------------| | assetId | number | Yes | Asset ID | ### Request Body `application/json` | Field | Type | Required | Description | |--------------|--------|----------|-------------------------| | thumbnail_url | string | Yes | New thumbnail URL | ### Response `200` ```json { "status": "success", "message": "Thumbnail updated.", "data": { ...asset } } ``` ### Response `400` ```json { "status": "error", "message": "thumbnail_url is required." } ``` --- ## Delete Asset **`DELETE /api/admin/assets/:assetId`** Soft deletes a single asset by setting `deletedAt` and `deletedBy`. ### Path Parameters | Parameter | Type | Required | Description | |-----------|--------|----------|-------------| | assetId | number | Yes | Asset ID | ### Request Body `application/json` | Field | Type | Required | Description | |----------|--------|----------|--------------------------------| | deletedBy | number | No | User ID of who deleted the asset | ### Response `200` ```json { "status": "success", "message": "Asset deleted." } ``` --- ## Bulk Delete Assets **`DELETE /api/admin/assets/bulk`** Soft deletes multiple assets at once. ### Request Body `application/json` | Field | Type | Required | Description | |----------|----------|----------|----------------------------------| | ids | number[] | Yes | Array of asset IDs to delete | | deletedBy | number | No | User ID of who deleted the assets | ### Response `200` ```json { "status": "success", "message": "3 asset(s) deleted." } ``` ### Response `400` ```json { "status": "error", "message": "ids must be a non-empty array." } ``` --- ## Restore Asset **`PATCH /api/admin/assets/:assetId/restore`** Restores a soft-deleted asset by clearing `deletedAt` and `deletedBy`. ### Path Parameters | Parameter | Type | Required | Description | |-----------|--------|----------|-------------| | assetId | number | Yes | Asset ID | ### Response `200` ```json { "status": "success", "message": "Asset restored.", "data": { ...asset } } ``` ### Response `404` ```json { "status": "error", "message": "Asset not found or not deleted." } ``` --- ## Error Responses All endpoints return the following on server error: ```json { "status": "error", "message": "Internal server error." } ``` --- ## File Size Limits | Type | Max Size | |----------|----------| | Images | 10 GB | | Videos | 10 GB | | Documents| 10 GB | > Limit is applied at the multer middleware level. Adjust in `assets.routes.js` if needed. --- ## Notes - **File replacement** is not supported. To replace a file, delete the old asset and upload a new one. - **Checksum** (SHA-256) is computed on upload for duplicate detection. - **Polymorphic ownership** via `owner_type` + `owner_id` allows any entity (`Course`, `User`, `Post`, etc.) to own assets without a direct foreign key. - **Resolution** is always auto-derived from `width` and `height` — never set manually. - **Soft delete** sets `deletedAt` timestamp. Assets are excluded from all queries unless explicitly queried with `paranoid: false`.