Module Storage — S3
Téléversement et stockage de fichiers dans un service compatible S3.
Vue d'ensemble
- ✅ AWS S3, MinIO, Backblaze B2, DigitalOcean Spaces, Cloudflare R2 ou autre service compatible ;
- ✅ isolation des clés par organisation ;
- ✅ URL présignées pour les accès temporaires ;
- ✅ interfaces TypeScript ;
- ✅ composants et intégration frontend prêts à l'emploi.
API
Le backend fournit un endpoint multipart, une arborescence par organisation, des URL de téléchargement, la suppression avec contrôle des permissions et une abstraction du fournisseur.
Web
Le frontend dispose du choix de fichier, du suivi de progression, du glisser-déposer via les composants ShadCN et de la connexion à l'API.
Options de configuration
1. MinIO avec Docker — développement
sf new # ou sf update
# Choisir « add MinIO with Docker »S3_ENDPOINT="http://localhost:9000"
S3_REGION="us-east-1"
S3_ACCESS_KEY_ID="minioadmin"
S3_SECRET_ACCESS_KEY="minioadmin"
S3_BUCKET="myapp-uploads"
S3_PUBLIC_URL="http://localhost:9000/myapp-uploads"MinIO tourne localement et expose sa console sur http://localhost:9001.
2. AWS S3 — production
Créez un bucket et un utilisateur IAM limité aux actions nécessaires :
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::your-bucket-name", "arn:aws:s3:::your-bucket-name/*"]
}
]
}S3_ENDPOINT="s3.amazonaws.com"
S3_REGION="us-east-1"
S3_ACCESS_KEY_ID="AKIAXXXXXXXX"
S3_SECRET_ACCESS_KEY="your-secret-key"
S3_BUCKET="your-bucket-name"
S3_PUBLIC_URL="https://your-bucket-name.s3.amazonaws.com"3. Autres fournisseurs S3
Adaptez l'endpoint, la région et l'URL publique. Exemple DigitalOcean Spaces :
S3_ENDPOINT="nyc3.digitaloceanspaces.com"
S3_REGION="us-east-1"
S3_ACCESS_KEY_ID="your-spaces-key"
S3_SECRET_ACCESS_KEY="your-spaces-secret"
S3_BUCKET="your-space-name"
S3_PUBLIC_URL="https://your-space-name.nyc3.digitaloceanspaces.com"Installation
sf new
# Activer Object storage (S3), puis choisir MinIO ou un service existant.
sf update
# Choisir Storage (S3), puis Docker / Credentials / Manual.L'installateur copie le module dans apps/api/src/modules/storage/, configure l'environnement, installe @aws-sdk/client-s3, enregistre StorageModule et active la capacité côté web.
API
Téléverser
POST /api/storage/upload
Content-Type: multipart/form-data
Authorization: Bearer {token}
file: [binary data]{
"id": "550e8400-e29b-41d4-a716-446655440000",
"key": "org-123/logo.png",
"bucket": "myapp-uploads",
"url": "https://myapp-uploads.s3.amazonaws.com/org-123/logo.png",
"publicUrl": "https://myapp-uploads.s3.amazonaws.com/org-123/logo.png"
}Obtenir une URL de téléchargement
GET /api/storage/:id/download
Authorization: Bearer {token}{
"url": "https://myapp-uploads.s3.amazonaws.com/org-123/logo.png?X-Amz-Signature=..."
}L'URL présignée reste valable une heure.
Supprimer
DELETE /api/storage/:id
Authorization: Bearer {token}Intégration backend
import { StorageService } from '@modules/storage/services/storage.service'
@Injectable()
export class OrganizationService {
constructor(private readonly storageService: StorageService) {}
async uploadLogo(user: User, file: Express.Multer.File) {
const result = await this.storageService.uploadFile(file, user.organizationId)
await this.prisma.organization.update({
where: { id: user.organizationId },
data: { logoUrl: result.publicUrl }
})
return result
}
}Passez toujours l'identifiant de l'organisation contrôlée par l'authentification ; ne faites pas confiance à une organisation fournie par le client.
Intégration frontend
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/storage/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${localStorage.getItem('accessToken')}` },
body: formData
})
const { publicUrl } = await response.json()
updateOrganization.mutate({ logoUrl: publicUrl })Le formulaire généré peut combiner Input type="file", validation client et bouton désactivé tant qu'aucun fichier n'est choisi.
Organisation des fichiers
your-bucket/
├── org-abc123/
│ ├── logo.png
│ ├── documents/contract.pdf
│ └── avatars/user-1.jpg
└── org-def456/
└── logo.pngFormat de clé : {organizationId}/{filename}. Cela sépare les tenants, simplifie le nettoyage d'une organisation et rend l'appartenance lisible.
Sécurité
- Upload : JWT obligatoire.
- Téléchargement : authentification et appartenance à l'organisation.
- Suppression : authentification et appartenance à l'organisation.
- Validation : taille, type MIME et propriétaire contrôlés côté serveur.
@Post('upload')
@UseGuards(JwtAuthGuard)
@UseInterceptors(
FileInterceptor('file', {
limits: { fileSize: 10 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (!file.mimetype.match(/\/(jpg|jpeg|png|pdf)$/)) {
return cb(new Error('Only images and PDFs allowed'), false)
}
cb(null, true)
}
})
)
async uploadFile(@CurrentUser() user: User, @UploadedFile() file: Express.Multer.File) {
return this.storageService.uploadFile(file, user.organizationId)
}Le type MIME déclaré par le client ne suffit pas pour un usage sensible : ajoutez une inspection du contenu si votre menace l'exige.
Variables d'environnement
S3_ENDPOINT="s3.amazonaws.com"
S3_REGION="us-east-1"
S3_ACCESS_KEY_ID="your-access-key"
S3_SECRET_ACCESS_KEY="your-secret-key"
S3_BUCKET="your-bucket"
S3_PUBLIC_URL="https://your-bucket.s3.amazonaws.com"
S3_MAX_FILE_SIZE="10485760"
S3_ALLOWED_MIME_TYPES="image/jpeg,image/png,application/pdf"Activez le frontend :
VITE_STORAGE_ENABLED="true"Développement avec MinIO
docker compose -f docker-compose.dev-services.yml up -d- console :
http://localhost:9001; - identifiant et mot de passe par défaut :
minioadmin; - bucket : créez
myapp-uploads, identique àS3_BUCKET.
Test :
curl -X POST http://localhost:3000/api/storage/upload \
-H "Authorization: Bearer {your-jwt-token}" \
-F "file=@/path/to/file.png"Production AWS
aws s3 mb s3://myapp-uploads --region us-east-1
aws iam create-user --user-name myapp-s3-user
aws iam put-user-policy --user-name myapp-s3-user \
--policy-name S3Access \
--policy-document file://s3-policy.json
aws iam create-access-key --user-name myapp-s3-userStockez les clés comme secrets de déploiement. Pour un CDN CloudFront, placez le bucket en origine et remplacez S3_PUBLIC_URL par le domaine de la distribution.
Dépannage
Invalid Credentials
Vérifiez l'identifiant, le secret et les permissions IAM exactes.
Bucket does not exist
Comparez S3_BUCKET au nom réel. Avec MinIO, créez d'abord le bucket dans la console.
Fichiers inaccessibles
Vérifiez la politique publique/privée, S3_PUBLIC_URL et CORS. En développement MinIO :
mc alias set myminio http://localhost:9000 minioadmin minioadmin
mc anonymous set download myminio/myapp-uploadsGros fichier refusé
Alignez les limites du contrôleur, de NestJS et du reverse proxy :
client_max_body_size 50M;Augmenter la limite ne remplace ni la validation ni une stratégie multipart pour de très gros objets.