Skip to content

Commit bb4e34a

Browse files
authored
Merge pull request #509 from omosvico/docs/api-files-uploads-437
docs(backend): add api-files-uploads.md documenting files & uploads endpoints
2 parents 9b13e62 + 7808449 commit bb4e34a

1 file changed

Lines changed: 281 additions & 0 deletions

File tree

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# Files & uploads API
2+
3+
This document describes every route exposed by `apps/backend/src/routes/uploads.ts` and `apps/backend/src/routes/files.ts`:
4+
5+
- `POST /uploads` — request a presigned upload slot
6+
- `POST /uploads/:fileId/confirm` — mark a file as ready after upload
7+
- `GET /files/:fileId` — obtain a presigned download URL
8+
9+
All three routes require authentication (see `POST /auth/verify` in [`api-auth.md`](api-auth.md)). The `requireAuth` middleware is applied at the router level.
10+
11+
---
12+
13+
## Size & MIME constraints
14+
15+
| Constraint | Value | Source |
16+
|---|---|---|
17+
| Max file size | **100 MB** (`100 * 1024 * 1024` bytes) | `routes/uploads.ts:14` |
18+
| Allowed MIME types | `image/jpeg`, `image/png`, `image/gif`, `image/webp` | `routes/uploads.ts:16-21` |
19+
| | `video/mp4`, `video/webm` | |
20+
| | `audio/mpeg`, `audio/ogg`, `audio/wav` | |
21+
| | `application/pdf`, `application/octet-stream` | |
22+
23+
Any MIME type outside this set is rejected with HTTP `415` during slot request.
24+
25+
---
26+
27+
## `POST /uploads`
28+
29+
Requests a presigned upload slot for a file. The caller must be a member of the target conversation.
30+
31+
**Auth:** Required (JWT from `POST /auth/verify`).
32+
33+
### Request body
34+
35+
```json
36+
{
37+
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
38+
"size": 4194304,
39+
"mimeType": "image/png",
40+
"sha256": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
41+
"isThumbnail": false
42+
}
43+
```
44+
45+
| Field | Type | Required | Description |
46+
|---|---|---|---|
47+
| `conversationId` | `string` (uuid) | yes | Target conversation UUID. |
48+
| `size` | `number` (int) | yes | File size in bytes. Must be 1 ≤ size ≤ 100,000,000. |
49+
| `mimeType` | `string` | yes | Must be in the allowed MIME types set (see above). |
50+
| `sha256` | `string` | yes | Hex-encoded SHA-256 hash of the file content. |
51+
| `isThumbnail` | `boolean` | no | Whether this is a thumbnail of a larger file. Defaults to `false`. |
52+
53+
### Responses
54+
55+
#### `201` — Slot created
56+
57+
```json
58+
{
59+
"fileId": "660e8400-e29b-41d4-a716-446655440001",
60+
"uploadUrl": "https://storage.example.com/uploads/conv-uuid/a1b2...c3d4?X-Expires=..."
61+
}
62+
```
63+
64+
| Field | Type | Description |
65+
|---|---|---|
66+
| `fileId` | `string` | UUID of the newly created file row (status: `pending`). |
67+
| `uploadUrl` | `string` | Presigned PUT URL. Valid for 15 minutes (production) or a fake URL (dev/test). |
68+
69+
The client must perform a `PUT` request to `uploadUrl` with the file bytes as the body and the declared `mimeType` as `Content-Type`. **There is no server-side storage verification** — see confirm step below.
70+
71+
#### `400` — Validation error
72+
73+
Returned when the request body fails schema validation (e.g., missing field, non-uuid `conversationId`, size exceeds max, etc.).
74+
75+
```json
76+
{
77+
"error": "Invalid request",
78+
"details": [
79+
{
80+
"code": "too_big",
81+
"path": ["size"],
82+
"message": "Number must be less than or equal to 100000000"
83+
}
84+
]
85+
}
86+
```
87+
88+
#### `403` — Not a conversation member
89+
90+
```json
91+
{ "error": "Not a member of this conversation" }
92+
```
93+
94+
#### `415` — Unsupported media type
95+
96+
```json
97+
{ "error": "Unsupported media type", "mimeType": "image/tiff" }
98+
```
99+
100+
---
101+
102+
## `POST /uploads/:fileId/confirm`
103+
104+
Marks a `pending` file as `ready`. The client should call this after successfully uploading the bytes to the presigned PUT URL.
105+
106+
**Auth:** Required (JWT from `POST /auth/verify`). Only the original uploader may confirm.
107+
108+
### Path parameter
109+
110+
| Parameter | Description |
111+
|---|---|
112+
| `fileId` | UUID of the file row returned by `POST /uploads`. |
113+
114+
### Responses
115+
116+
#### `200` — Confirmed
117+
118+
```json
119+
{
120+
"fileId": "660e8400-e29b-41d4-a716-446655440001",
121+
"status": "ready"
122+
}
123+
```
124+
125+
The file is now eligible to be referenced in a message.
126+
127+
#### `400` — Missing fileId
128+
129+
```json
130+
{ "error": "fileId is required" }
131+
```
132+
133+
#### `403` — Not authorized
134+
135+
Returned when the authenticated user is not the original uploader.
136+
137+
```json
138+
{ "error": "Not authorized to confirm this upload" }
139+
```
140+
141+
#### `404` — File not found
142+
143+
```json
144+
{ "error": "File not found" }
145+
```
146+
147+
#### `409` — Already ready
148+
149+
```json
150+
{ "error": "File is already ready" }
151+
```
152+
153+
#### `409` — Deleted
154+
155+
```json
156+
{ "error": "File has been deleted" }
157+
```
158+
159+
### What confirm does and does not verify
160+
161+
**Does:**
162+
- Asserts the requesting user is the original uploader (`uploaderId` check).
163+
- Asserts the file exists and is in a confirmable state (`pending`).
164+
- Transitions status from `pending``ready`.
165+
166+
**Does NOT:**
167+
- Verify the file bytes were actually uploaded to storage.
168+
- Check the uploaded content's SHA-256 hash against the value declared in the slot request.
169+
- Re-check file size or MIME type against the stored metadata.
170+
- Perform any integrity check (hash comparison, storage HEAD request, etc.).
171+
172+
The confirm handler is a pure database state transition: it trusts that the client successfully performed the PUT and does not reach out to the object store to verify.
173+
174+
---
175+
176+
## `GET /files/:fileId`
177+
178+
Issues a short-lived presigned GET URL so the client can download the file (ciphertext) and decrypt it locally.
179+
180+
**Auth:** Required (JWT from `POST /auth/verify`). The caller must be a member of the conversation where the file was shared.
181+
182+
### Path parameter
183+
184+
| Parameter | Description |
185+
|---|---|
186+
| `fileId` | UUID of the file row. |
187+
188+
### Responses
189+
190+
#### `200` — URL issued
191+
192+
```json
193+
{
194+
"url": "https://storage.example.com/uploads/conv-uuid/a1b2...c3d4?X-Expires=..."
195+
}
196+
```
197+
198+
The presigned URL is valid for **5 minutes** (300 seconds). The client should start the download immediately. In dev/test environments the URL is a structurally-plausible fake (see `storage.ts:15-19`).
199+
200+
| Field | Type | Description |
201+
|---|---|---|
202+
| `url` | `string` | Presigned GET URL. |
203+
204+
#### `400` — Missing fileId
205+
206+
```json
207+
{ "error": "File id is required" }
208+
```
209+
210+
#### `403` — Not authorized
211+
212+
Returned when the authenticated user is not a member of the conversation that contains the referencing message.
213+
214+
```json
215+
{ "error": "Not authorized to access this file" }
216+
```
217+
218+
#### `404` — File not found
219+
220+
Returned when the file row does not exist, has been soft-deleted (`deletedAt` is set), or is not referenced by any message.
221+
222+
```json
223+
{ "error": "File not found" }
224+
```
225+
226+
Also returned (same shape) when the file exists but no message references it:
227+
228+
```json
229+
{ "error": "File not referenced by any message" }
230+
```
231+
232+
#### `500` — Download URL generation failed
233+
234+
```json
235+
{ "error": "Failed to generate download URL" }
236+
```
237+
238+
### Access control flow
239+
240+
1. File existence and `deletedAt` are checked first.
241+
2. The message referencing the file is looked up (via `messages.fileId`).
242+
3. Conversation membership is verified against `message.conversationId`.
243+
4. A presigned GET URL is generated for the file's `storageKey`.
244+
245+
---
246+
247+
## File lifecycle
248+
249+
```
250+
PUT /uploads POST /uploads/:id/confirm
251+
(client ──→ storage) (client ──→ backend)
252+
│ │
253+
▼ ▼
254+
[slot allocated] ──→ pending ──────────────────→ ready
255+
256+
[file message references it]
257+
258+
259+
deleted
260+
(soft, via retraction)
261+
262+
263+
hardDeletedAt
264+
(background cleanup)
265+
```
266+
267+
- `pending` — slot allocated, not yet confirmed.
268+
- `ready` — confirm handler called; file is eligible for message attachment.
269+
- `deleted` — soft-deleted (`deletedAt` set) when referencing messages are retracted.
270+
- Files with `deletedAt` set are excluded from `GET /files/:fileId` (returns `404`).
271+
- `hardDeletedAt` is set by a background cleanup job after no live references remain.
272+
273+
---
274+
275+
## Implementation references
276+
277+
- Upload slot + confirm routes: `apps/backend/src/routes/uploads.ts`
278+
- Download route: `apps/backend/src/routes/files.ts`
279+
- Presigned URL generation (S3/MinIO / dev fallback): `apps/backend/src/lib/storage.ts`
280+
- Object store abstraction: `apps/backend/src/lib/objectStore.ts`
281+
- Database schema (files table): `apps/backend/src/db/schema.ts` (lines 86–105)

0 commit comments

Comments
 (0)