Ugrás a fő tartalomhoz

Avatar tárolási architektúra

Ez a dokumentum a profilkép (avatar) tárolás technikai megvalósítását írja le a CADENSA rendszerben, Wasabi S3 objektumtárolás használatával.


Áttekintés

A profilképek Wasabi S3 felhőtárhelyen tárolódnak, nem a helyi fájlrendszeren vagy az adatbázisban. A backend minden feltöltött képet feldolgoz mielőtt tárolná: 400×400 pixelre méretezi, WebP formátumra konvertálja, majd az optimalizált fájlt feltölti a cadensa-avatars bucketbe.

A tárolt képekhez való hozzáférés előre aláírt URL-eken (pre-signed URL) keresztül történik, 7 napos lejárattal. A nyers Wasabi objektumkulcs kerül az adatbázisba; az aláírt URL minden API válasz esetén generálódik.


Infrastruktúra

TulajdonságÉrték
SzolgáltatóWasabi
Régióeu-central-2
Bucketcadensa-avatars
Object LockKikapcsolva
Publikus hozzáférés❌ Nem (csak előre aláírt URL-en keresztül)
Signed URL lejárat7 nap

Környezeti változók

WASABI_ACCESS_KEY_ID=...
WASABI_SECRET_ACCESS_KEY=...
WASABI_REGION=eu-central-2
WASABI_ENDPOINT=https://s3.eu-central-2.wasabisys.com
WASABI_BUCKET=cadensa-files # általános célú bucket
WASABI_BUCKET_AVATARS=cadensa-avatars

Feltöltési pipeline

Kliens (böngésző/app)

│ POST /api/upload/avatar (multipart/form-data)

multer (memoryStorage)
│ req.file.buffer ← kép RAM-ban, soha nem kerül lemezre

avatarOptimizer.ts
│ sharp: 400×400 cover crop átméretezés
│ WebP konverzió, 80% minőség
│ visszaad: { buffer, contentType: 'image/webp', ext: '.webp', originalSize, optimizedSize }

wasabi-s3.service.ts → uploadAvatar()
│ kulcs: avatars/{userId}/{timestamp}.webp
│ bucket: cadensa-avatars

Adatbázis
│ user.avatar = "wasabi:avatars/{userId}/{timestamp}.webp"

API válasz
│ url: "wasabi:avatars/..." (tárolt kulcs, DB-hez)
│ signedUrl: "https://..." (előre aláírt, 7 nap)

Objektumkulcs formátuma

Az objektumkulcsok a következő mintát követik:

avatars/{userId}/{unixTimestampMs}.webp

Példa:

avatars/6627a3f1e2b4c10012345678/1748000000000.webp

A MongoDB-ben a user.avatar mezőben tárolt érték wasabi: előtaggal van ellátva:

wasabi:avatars/6627a3f1e2b4c10012345678/1748000000000.webp

Ez az előtag lehetővé teszi a backend számára, hogy megkülönböztesse a Wasabi kulcsokat a régebbi adatoktól (pl. Gravatar URL-ek, helyi fájlnevek).


Képoptimalizálás

Megvalósítva a cadensa-backend/src/services/upload/avatarOptimizer.ts fájlban, a sharp library használatával.

ParaméterÉrték
Méretek400 × 400 px
Illesztéscover (középre igazított vágás)
FormátumWebP
Minőség80%
Tipikus méretcsökkentés~95–97% az eredeti JPEG/PNG-hez képest
// avatarOptimizer.ts (egyszerűsítve)
export async function optimizeAvatar(buffer: Buffer, originalSize: number) {
const optimized = await sharp(buffer)
.resize(400, 400, { fit: 'cover', position: 'centre' })
.webp({ quality: 80 })
.toBuffer();

return {
buffer: optimized,
contentType: 'image/webp',
ext: '.webp',
originalSize,
optimizedSize: optimized.length,
};
}

Előre aláírt URL folyamat

Mivel a cadensa-avatars bucket nem publikusan elérhető, minden olvasáshoz előre aláírt URL szükséges.

1. Kliens meghív egy végpontot, amely felhasználói adatokat ad vissza
(GET /auth/me, GET /user/profile, GET /team/:id/members, …)

2. Backend feloldja a nyers avatar kulcsot:
resolveAvatarUrl("wasabi:avatars/...") → getSignedUrl(...)

3. Az AWS SDK generál egy ideiglenes aláírt URL-t (7 napos lejárat)

4. Az aláírt URL visszakerül a kliensnek a JSON válaszban

5. A kliens közvetlenül az aláírt URL-ről jeleníti meg a képet

Az aláírt URL lekérdezési paramétereiben tartalmazza a hitelesítési adatokat, és 7 nap után automatikusan lejár. A következő API hívásnál új aláírt URL generálódik.


Főbb forrásfájlok

FájlFeladat
src/services/storage/wasabi-s3.service.tsS3 kliens, uploadAvatar(), deleteAvatar(), getAvatarSignedUrl()
src/services/upload/avatarOptimizer.tssharp-alapú képátméretezés + WebP konverzió
src/services/upload/Upload.service.tsmulter konfiguráció (memoryStorage)
src/controllers/upload.controller.tsPOST /api/upload/avatar kezelő
src/utils/avatarResolver.tsresolveAvatarUrl() + resolveAvatarsInList() segédfüggvények

avatarResolver.ts használata

A resolveAvatarUrl() az adatbázis nyers értékét aláírt URL-lé alakítja (vagy üres stringet ad vissza üres/nem-Wasabi értékek esetén):

import { resolveAvatarUrl, resolveAvatarsInList } from '../utils/avatarResolver';

// Egy felhasználó
const signedUrl = await resolveAvatarUrl(user.avatar);

// Csapattagok listája
await resolveAvatarsInList(
members,
(m) => m.avatar,
(m, url) => { m.avatar = url; }
);

Mindig hívd meg ezeket a segédfüggvényeket az API válasz elküldése előtt, hogy a kliensek soha ne kapjanak nyers wasabi: kulcsot.


Controllerek, amelyek feloldják az avatarokat

ControllerVégpontFeloldás helye
auth.controller.tsPOST /auth/loginBejelentkezés után, JWT válasz előtt
auth.controller.tsGET /auth/mefetchCurrentUser kezelőben
user.controller.tsGET /user/profilegetProfile kezelőben
team.controller.tsGET /team/:id/membersenrichedMembers lista felépítése után

Frontend kezelés

  • Sikeres feltöltés után a frontend megkapja az url (a wasabi: kulcs) és a signedUrl (az ideiglenes képURL) értékeket.
  • A wasabi: kulcs kerül mentésre a felhasználói profilban (a jövőbeli API hívásokhoz).
  • A signedUrl az azonnali helyi megjelenítéshez használatos, extra API körút nélkül.
  • Az avatarUtils.ts és avatarUrl.ts undefined-t ad vissza minden wasabi:-val kezdődő értékre, így a komponens kezdőbetűkre esik vissza — a feloldott aláírt URL mindig az API-tól kell érkezzen, nem a Redux store-ból.

Avatar törlése

Amikor a felhasználó eltávolítja a profilképét, a deleteAvatar(key) meghívódik a Wasabi szolgáltatáson, mielőtt a user.avatar mező törlődne az adatbázisban:

await wasabiService.deleteAvatar('avatars/{userId}/{timestamp}.webp');

A delete metódusnak a wasabi: előtag nélküli nyers kulcs kerül átadásra.