Skip to content

Commit ad6558d

Browse files
committed
Add image upload and retrieval API with Vercel Blob integration
1 parent b8f98db commit ad6558d

8 files changed

Lines changed: 411 additions & 8 deletions

File tree

README.md

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,194 @@ kryptosphere-api/
158158

159159
## 🔌 Routes API
160160

161-
| Méthode | Route | Description | Auth |
162-
|---------|-------|-------------|------|
163-
| `POST` | `/api/auth/login` | Authentification ||
164-
| `GET` | `/api/auth/me` | Récupérer l'utilisateur connecté | ✅ Session |
165-
| `POST` | `/api/board` | Créer un board | ✅ SuperAdmin |
166-
| `POST` | `/api/setup` | Initialiser le root user | 🔑 SETUP_SECRET |
167-
| `GET` | `/api/health` | Healthcheck API & DB ||
161+
### Authentification
162+
163+
- **`POST /api/auth/login`**
164+
- **But**: Authentifier un utilisateur, renvoyer un `session` ID.
165+
- **Body**:
166+
```json
167+
{
168+
"login": "admin",
169+
"password": "votre_mot_de_passe"
170+
}
171+
```
172+
- **Réponse**:
173+
```json
174+
{ "session": "SESSION_ID_ICI" }
175+
```
176+
177+
- **`GET /api/auth/me`**
178+
- **But**: Récupérer l'utilisateur actuellement connecté.
179+
- **Headers**:
180+
- `Authorization: Bearer SESSION_ID_ICI`
181+
- **Réponse**:
182+
```json
183+
{
184+
"_id": "...",
185+
"login": "admin",
186+
"email": "...",
187+
"role": "SuperAdmin",
188+
"firstName": "...",
189+
"lastName": "...",
190+
"createdAt": "...",
191+
"updatedAt": "..."
192+
}
193+
```
194+
195+
### Board
196+
197+
- **`POST /api/board`**
198+
- **But**: Créer un board (réservé au `SuperAdmin`).
199+
- **Headers**:
200+
- `Authorization: Bearer SESSION_ID_ICI`
201+
- **Body**:
202+
```json
203+
{
204+
"name": "Board 2025",
205+
"year": 2025,
206+
"type": "main_board" // ou "chapter_board"
207+
}
208+
```
209+
- **Réponse** (200):
210+
```json
211+
{
212+
"_id": "...",
213+
"name": "Board 2025",
214+
"year": 2025,
215+
"type": "main_board",
216+
"createdAt": "...",
217+
"updatedAt": "..."
218+
}
219+
```
220+
221+
### Setup (création du root user)
222+
223+
- **`POST /api/setup`**
224+
- **But**: Créer l'utilisateur root (`SuperAdmin`), **une seule fois**.
225+
- **Headers ou body**:
226+
- `setupSecret` doit correspondre à la variable d'env `SETUP_SECRET`
227+
- **Body exemple**:
228+
```json
229+
{
230+
"setupSecret": "VOTRE_SETUP_SECRET",
231+
"login": "root",
232+
"password": "VOTRE_MOT_DE_PASSE_SECURISE",
233+
"email": "admin@votre-domaine.com",
234+
"firstName": "Admin",
235+
"lastName": "User"
236+
}
237+
```
238+
239+
### Healthcheck
240+
241+
- **`GET /api/health`**
242+
- **But**: Vérifier que l'API et MongoDB répondent.
243+
- **Réponse**:
244+
```json
245+
{
246+
"status": "ok",
247+
"db": "up",
248+
"timestamp": "...",
249+
"uptime": 123.45,
250+
"responseTimeMs": 10
251+
}
252+
```
253+
254+
### Images (Vercel Blob + Mongo) – intégration avec le website
255+
256+
Ces routes permettent au **website** de stocker et récupérer des images en centralisant la logique dans l’API.
257+
258+
#### 1. Upload d'une image
259+
260+
- **`POST /api/images`**
261+
- **But**: Uploader une image vers Vercel Blob et stocker ses métadonnées dans MongoDB.
262+
- **Auth**:
263+
- **Obligatoire**: `Authorization: Bearer SESSION_ID_ICI` (seuls les utilisateurs authentifiés peuvent uploader).
264+
- **Content-Type**: `multipart/form-data`
265+
- **Champs attendus**:
266+
- `file`: `File` (obligatoire) – le fichier image
267+
- `key`: `string` (obligatoire) – identifiant unique connu par le website (ex: `"homepage-hero"`, `"about-banner"`)
268+
- `altText`: `string` (optionnel)
269+
- `description`: `string` (optionnel)
270+
- **Exemple (local)**:
271+
```bash
272+
curl -X POST BASE_URL/api/images \
273+
-F "file=@img/image.png" \
274+
-F "key=homepage-hero" \
275+
-F "altText=Hero Kryptosphere" \
276+
-F "description=Image de hero de la home"
277+
```
278+
- **Réponse** (201):
279+
```json
280+
{
281+
"image": {
282+
"_id": "698b5eca89ff2c552ead1159",
283+
"key": "homepage-hero",
284+
"url": "https://...blob.vercel-storage.com/...",
285+
"altText": "Hero Kryptosphere",
286+
"description": "Image de hero de la home",
287+
"createdAt": "...",
288+
"updatedAt": "..."
289+
},
290+
"blob": {
291+
"url": "https://...blob.vercel-storage.com/..."
292+
}
293+
}
294+
```
295+
296+
**Important (fonctionnement avec le website)** :
297+
298+
- Le **website choisit** la valeur de `key` au moment de l'upload (`homepage-hero`, `about-banner`, etc.).
299+
- L'API stocke `key` + `url` Blob + métadonnées en base.
300+
- Le website n’a **pas besoin de connaître le Mongo `_id`**, seulement le `key`.
301+
302+
#### 2. Récupérer une image par `key`
303+
304+
- **`GET /api/images?key=<KEY>`**
305+
- **But**: Récupérer les métadonnées et l’URL d’une image à partir d’un `key` partagé avec le website.
306+
- **Exemple**:
307+
```bash
308+
curl "http://BASE_URL/api/images?key=homepage-hero"
309+
```
310+
- **Réponse**:
311+
```json
312+
{
313+
"image": {
314+
"_id": "698b5eca89ff2c552ead1159",
315+
"key": "homepage-hero",
316+
"url": "https://...blob.vercel-storage.com/...",
317+
"altText": "Hero Kryptosphere",
318+
"description": "Image de hero de la home",
319+
"createdAt": "...",
320+
"updatedAt": "..."
321+
}
322+
}
323+
```
324+
325+
- **Utilisation côté website** (pattern A) :
326+
327+
```ts
328+
const res = await fetch(`${API_BASE_URL}/api/images?key=homepage-hero`);
329+
const { image } = await res.json();
330+
331+
// Exemple React / Next.js
332+
<img src={image.url} alt={image.altText ?? ""} />
333+
```
334+
335+
#### 3. Lister des images (optionnel)
336+
337+
- **`GET /api/images?limit=20`**
338+
- **But**: Récupérer une liste d’images (par défaut 50, max 200).
339+
- **Réponse**:
340+
```json
341+
{
342+
"images": [
343+
{ "_id": "...", "key": "homepage-hero", "url": "https://...", ... },
344+
{ "_id": "...", "key": "about-banner", "url": "https://...", ... }
345+
]
346+
}
347+
```
348+
168349

169350
## 🛠️ Développement local (avec `npx vercel dev`)
170351

api/images/index.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { put } from "@vercel/blob";
2+
import { connectMongo } from "../../lib/mongodb";
3+
import { MongooseService } from "../../services/mongoose/mongoose.service";
4+
import { jsonResponse, errorResponse, verifySession, sendUnauthorized } from "../../lib/middleware";
5+
6+
/**
7+
* Upload an image to Vercel Blob and store metadata in MongoDB.
8+
*
9+
* POST /api/images
10+
* Content-Type: multipart/form-data
11+
* Fields:
12+
* - file: File (required)
13+
* - key: string (required, unique identifier known by the website)
14+
* - altText: string (optional)
15+
* - description: string (optional)
16+
*/
17+
export async function POST(request: Request): Promise<Response> {
18+
try {
19+
// Ensure DB connection is ready (for metadata storage)
20+
await connectMongo();
21+
22+
// Require authenticated user (restrict uploads)
23+
const user = await verifySession(request);
24+
if (!user) {
25+
return sendUnauthorized();
26+
}
27+
28+
const formData = await request.formData();
29+
const file = formData.get("file");
30+
31+
if (!file || !(file instanceof File)) {
32+
return errorResponse("Missing or invalid 'file' field (multipart/form-data expected)", 400);
33+
}
34+
35+
const key = formData.get("key");
36+
if (!key || typeof key !== "string") {
37+
return errorResponse("Missing or invalid 'key' field (string expected)", 400);
38+
}
39+
40+
const altText = (formData.get("altText") as string) || undefined;
41+
const description = (formData.get("description") as string) || undefined;
42+
43+
// Upload file to Vercel Blob
44+
const blob = await put(`images/${Date.now()}-${file.name}`, file, {
45+
access: "public",
46+
});
47+
48+
// Store metadata in MongoDB
49+
const mongooseService = await MongooseService.getInstance();
50+
const imageServices = mongooseService.imageServices;
51+
52+
const image = await imageServices.createImage({
53+
key,
54+
url: blob.url,
55+
altText,
56+
description,
57+
});
58+
59+
return jsonResponse(
60+
{
61+
image,
62+
blob: {
63+
url: blob.url,
64+
},
65+
},
66+
201
67+
);
68+
} catch (error) {
69+
console.error("Image upload error:", error);
70+
return errorResponse("Failed to upload image", 500);
71+
}
72+
}
73+
74+
/**
75+
* Get images metadata.
76+
*
77+
* GET /api/images
78+
* Query params:
79+
* - id: string (optional) → return a single image by Mongo _id
80+
* - limit: number (optional, default 50) → when listing images
81+
*
82+
* Usage côté website (pattern A) :
83+
* - appeler /api/images?id=<id> ou /api/images?limit=20
84+
* - utiliser image.url comme src dans les <img />.
85+
*/
86+
export async function GET(request: Request): Promise<Response> {
87+
try {
88+
await connectMongo();
89+
90+
const url = new URL(request.url);
91+
const id = url.searchParams.get("id");
92+
const limitParam = url.searchParams.get("limit");
93+
94+
const mongooseService = await MongooseService.getInstance();
95+
const imageServices = mongooseService.imageServices;
96+
97+
if (id) {
98+
const image = await imageServices.findImageById(id);
99+
if (!image) {
100+
return errorResponse("Image not found", 404);
101+
}
102+
return jsonResponse({ image });
103+
}
104+
105+
const limit = limitParam ? Math.min(parseInt(limitParam, 10) || 50, 200) : 50;
106+
const images = await imageServices.listImages(limit);
107+
return jsonResponse({ images });
108+
} catch (error) {
109+
console.error("Image fetch error:", error);
110+
return errorResponse("Failed to fetch images", 500);
111+
}
112+
}

models/image.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ITimestamp } from "./timestamp.interface";
33

44
export interface IImage extends ITimestamp {
55
_id: Types.ObjectId;
6+
key: string;
67
url: string;
78
altText?: string;
89
description?: string;

0 commit comments

Comments
 (0)