--- type: api title: Uploads Endpoints description: Admin-only multipart upload endpoints for site logos, category icons, and challenge files. tags: [api, uploads, multipart, admin] timestamp: 2026-07-22T19:18:00Z --- # Endpoints | Method | Path | Auth | Source | |--------|---------------------------------------|-------|---------------------------------------------------------| | `POST` | `/api/v1/uploads/logo` | Admin | `backend/src/modules/uploads/uploads.controller.ts` | | `POST` | `/api/v1/uploads/category-icon` | Admin | Same. | | `POST` | `/api/v1/uploads/challenge-file` | Admin | Same. *(Removed in Job 861 — challenge attachments now flow through `POST /api/v1/admin/challenges/files/stage` so they are only persisted on challenge save.)* | # Guard chain All handlers are gated by the controller-level `AdminGuard` and `admin` role. The global authentication and CSRF protections also apply; clients must send a valid admin session and the standard CSRF cookie/header pair. # Behavior * Each endpoint uses `FileInterceptor('file')` and parses a single multipart part named `file`. * File size is validated against `UPLOAD_SIZE_LIMIT` through `parseUploadSizeLimit()` in `backend/src/common/utils/upload.ts`. * Category icons are written under `UPLOAD_DIR/icons`; when `categoryId` is supplied, the image is cropped to a 128-by-128 PNG and exposed at `/uploads/icons/{categoryId}.png`. * Challenge attachments are written under `UPLOAD_DIR/challenges` and exposed at `/uploads/challenges/{safeFilename}`. * Site logos are decoded with Sharp and accepted only when their actual format is PNG, JPEG, GIF, or WebP. The multipart MIME type and filename extension are not trusted for validation. * An accepted logo keeps its sanitized filename, is written directly under `UPLOAD_DIR`, and returns `{ publicUrl, originalFilename }`, with `publicUrl` in the form `/uploads/{safeFilename}`. * Category icons are decoded with Sharp and normalized to a 128-by-128 PNG. When `categoryId` is supplied, the file is written to `UPLOAD_DIR/icons/{categoryId}.png` (overwriting any previous icon for that category) and the response carries `{ id: "{categoryId}.png", publicUrl, width: 128, height: 128, mimeType: "image/png", storedPath, size, originalFilename }`. * Category-icon validation is strict: a missing `file`, an oversized upload, a corrupt/non-image payload, or a buffer that Sharp cannot decode returns `400 Bad Request`. Invalid category-icon payloads use the message `Category icon must be a valid PNG, JPEG, GIF, or WebP image.` and **are not persisted** — the existing icon file (if any) on disk is left untouched. The multipart MIME type, the filename extension, and any browser `accept` metadata are not trusted as proof of validity. * A missing `file` part, an oversized upload, a corrupt image, or a decoded logo in another format returns `400 Bad Request`. Invalid logo payloads use the message `Logo must be a valid PNG, JPEG, GIF, or WebP image.` and are not persisted. # Examples 1. Sign in as an administrator and navigate to `/admin/general`. 2. In the site-logo setting, choose a PNG, JPEG, GIF, or WebP image and save the general settings. 3. The client uploads the selected file to `POST /api/v1/uploads/logo` before saving the returned `/uploads/...` URL as the configured logo. 4. After a successful save, the uploaded image is available from its returned public URL and is used wherever the configured site logo is rendered. 5. Choosing a corrupt file, or a valid image in an unsupported format, leaves the file unstored and surfaces the upload validation failure to the user. # Key files | File | Responsibility | |---|---| | `backend/src/modules/uploads/uploads.controller.ts` | Registers upload routes, validates payloads, and persists files. | | `backend/src/common/utils/upload.ts` | Parses size limits and sanitizes upload filenames. | | `frontend/src/app/features/admin/general/admin-general.component.ts` | Provides the administrator-facing logo selection and settings workflow. | | `tests/backend/uploads-logo.spec.ts` | Verifies accepted logo formats, spoofed/corrupt payload rejection, size limits, and persisted responses. | # See also - [Admin — General Settings](/guides/admin-general-settings.md) - [REST API Overview](/api/rest-overview.md) - [Backend Module Map](/architecture/backend-modules.md)