File/Image Upload
Overview
The platform provides a temporary (temp) file upload service shared by two main features: the file upload field type in forms, and image attachments on bulletin board posts and comments.
The principle is simple: files are uploaded to the API as soon as the user picks them, and only the returned path or URL is stored in the field value or the post payload. Binding a temporary file to a real record happens on the API side at save time.
Business Flow
- The user picks a file, either through the Ant Design upload component on the form side or through a standard file input on the bulletin board side.
- The component invokes its own upload handler rather than Ant Design's default upload mechanism, which keeps control over the API call and error handling in application code.
- The app assembles the payload and calls the upload service, which chooses its endpoint based on the payload shape:
- FormData goes to
POST /upload-file/tempas multipart. - A base64 string goes to
POST /upload-file/temp-base64as JSON.
- FormData goes to
- The response is set as the value of that form field.
- When the user submits the form, the value normalization step reduces the stored value to just the path or file name before sending.
- Users can remove an attached file, which clears the field value back to empty.
- On the bulletin board side, successfully uploaded images are collected as a list of URLs and sent with the post payload, subject to per-post and per-comment image count limits configured for that OA.
Key Screens & Components
Shared service
- The upload service (
src/service/upload-file.service.ts) is the single point of contact with the upload endpoints, supporting both multipart and base64 payloads.
Form-side components
- The primary upload component (
src/components/form-builder/upload/upload-1.component.tsx) handles the API call, file removal, and display of the uploaded file name. - A legacy upload component (
src/components/form-builder/file-upload.component.tsx) remains in the project to support forms configured in the older style.
Bulletin-side components (all under src/app/[hash]/bulletin/components/)
- The compose sheet and the comment composer call the upload service directly to attach images.
- The image grid component is responsible for displaying already-attached images.
Endpoints used
| Method | Path | Payload format |
|---|---|---|
| POST | /upload-file/temp | multipart/form-data |
| POST | /upload-file/temp-base64 | application/json |
Dependencies
- Ant Design Upload and Dragger provide the underlying upload UI on the form side.
- Accepted file extensions are configured per field in the CMS and arrive with the form definition.
- Consumed by Form Filling, Bulletin Board, and Bulletin Post Detail.
- Files uploaded through these endpoints are temporary; associating them with a real record is the API's responsibility at save time.
Backend Details (Client API)
The backend upload service never touches the database — it works only against object storage — and both endpoints are public routes requiring no LIFF token. That fact matters a great deal, and must be read alongside the commit step at save time, which is what actually proves ownership of a file.
File checks on the multipart upload (POST /api/upload-file/temp)
- A missing or unreadable
filefield → 400file must be a file. - Larger than 10 MB → 400, with a message stating the maximum size in bytes.
- The backend determines the file type from the file's actual contents, never from the browser's
Content-Type— renaming a file or forging the header does not get it through. - Only JPEG, PNG, and PDF are accepted; anything else → 400
file has invalid mime type. - The file is written to the temporary folder as
temp/temp-{milliseconds}.{extension}and its public URL is returned.
A caveat for the web app: the accepted-extensions list configured per field in the CMS is only a UI hint. The real gate is the backend's allowed type set. If an admin configures other types (docx, xlsx), users will be able to pick such a file and then have it rejected at upload time.
How the base64 endpoint differs (POST /api/upload-file/temp-base64)
- It supports a wider set of types than the multipart endpoint: jpg, png, gif, pdf, and webp — detected from the file signature; when detection fails it returns 400 with a message listing the supported types.
- There is no 10 MB limit on this path, a clear divergence from the multipart endpoint.
- The data URI header is stripped automatically (everything before the first comma), so a full data URI can be sent.
- Decoded content that is empty → 400
Empty file content. - Parity note: this endpoint also copies the file to a path that nothing uses, left over from the legacy system and ported deliberately. The returned value is still the URL of the file in the temporary folder, not of that copy.
What happens next (more important than the endpoint itself)
Because the upload endpoints require no authentication, ownership is established at save time instead — and each feature does it differently:
- Forms: at submit time the backend copies the file from the temporary folder into the form's permanent location, with filenames restricted to letters, digits, dots, underscores, and hyphens. Paths outside the temporary folder are silently skipped.
- Bulletin board: stricter — it verifies with storage that the object actually exists before copying. This is the only gate preventing a reference to another tenant's object or a key that was never uploaded, and it accepts either a public URL or a raw key matching the temporary filename pattern.
Security notes worth knowing
- Both endpoints are public. Anyone can upload into the temporary folder without a token; the only thing bounding volume is the application-level per-IP rate limit (see App Shell & Core Providers).
- Detecting the type from file contents rather than trusting the client header directly defeats extension-spoofing uploads.
- The form path does not verify object existence before copying the way the bulletin path does — a difference between the two features worth knowing.
Edge cases worth knowing
- Nothing in the request path ever deletes files from the temporary folder. A user who uploads and then abandons the form leaves the file behind permanently — sweeping that folder is a separate operations task, not something the app does.
- Stored filenames are regenerated from a millisecond timestamp; the user's original filename is not retained. If the web app needs to display it, it must remember it client-side.
- Because filenames are millisecond-based, simultaneous uploads within the same millisecond can in theory collide.
- If object storage is not configured, uploads simply fail — there is no fallback location.