template-saas/docs/01-modulos/SAAS-011-storage.md
rckrdmrd 4dafffa386 feat: Add superadmin metrics, onboarding and module documentation
- Add MetricsPage and useOnboarding hook
- Update superadmin controller and service
- Add module documentation (docs/01-modulos/)
- Add CONTEXT-MAP.yml and Sprint 5 execution report
- Update project status and task traces

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:40:26 -06:00

264 lines
6.1 KiB
Markdown

# SAAS-011: Storage
## Metadata
- **Codigo:** SAAS-011
- **Modulo:** Storage
- **Prioridad:** P1
- **Estado:** Pendiente
- **Fase:** 3 - Features Core
## Descripcion
Sistema de almacenamiento de archivos: upload/download con presigned URLs, organizacion por tenant, limites por plan, y soporte multi-provider (S3, R2, MinIO).
## Objetivos
1. Upload seguro con presigned URLs
2. Organizacion por tenant
3. Limites de almacenamiento por plan
4. Soporte multi-provider
5. Procesamiento de imagenes
## Alcance
### Incluido
- Upload via presigned URLs
- Download via presigned URLs
- Organizacion: tenant/tipo/archivo
- Tracking de uso por tenant
- Limites por plan
- Thumbnails automaticos (imagenes)
- Tipos MIME permitidos
### Excluido
- CDN propio - usar Cloudflare
- Streaming de video
- Compresion de archivos
## Proveedores Soportados
| Proveedor | Uso | Config |
|-----------|-----|--------|
| AWS S3 | Produccion | Bucket por region |
| Cloudflare R2 | Produccion | Sin egress fees |
| MinIO | Desarrollo | Docker local |
## Modelo de Datos
### Tablas (schema: storage)
**files**
- id, tenant_id, uploaded_by
- filename, original_name
- mime_type, size_bytes
- path, bucket
- metadata (JSONB)
- created_at
**storage_usage**
- id, tenant_id
- total_files, total_bytes
- updated_at
## Endpoints API
| Metodo | Endpoint | Descripcion |
|--------|----------|-------------|
| POST | /storage/upload-url | Obtener presigned upload URL |
| POST | /storage/confirm | Confirmar upload exitoso |
| GET | /storage/files | Listar archivos |
| GET | /storage/files/:id | Metadata de archivo |
| GET | /storage/files/:id/download | Presigned download URL |
| DELETE | /storage/files/:id | Eliminar archivo |
| GET | /storage/usage | Uso actual |
## Flujo de Upload
```
1. Cliente solicita upload URL
POST /storage/upload-url
Body: { filename, mimeType, size }
2. Backend valida:
- Tipo MIME permitido
- Tamaño dentro de limite
- Espacio disponible
3. Backend genera presigned URL
- Expira en 15 minutos
- Limite de tamaño
4. Cliente sube directo a S3/R2
PUT [presigned-url]
Body: [file binary]
5. Cliente confirma
POST /storage/confirm
Body: { uploadId }
6. Backend registra archivo
- Actualiza storage_usage
```
## Implementacion
### Servicio
```typescript
interface StorageService {
getUploadUrl(params: UploadParams): Promise<PresignedUrl>;
confirmUpload(uploadId: string): Promise<FileRecord>;
getDownloadUrl(fileId: string): Promise<string>;
deleteFile(fileId: string): Promise<void>;
getUsage(tenantId: string): Promise<StorageUsage>;
}
interface UploadParams {
filename: string;
mimeType: string;
sizeBytes: number;
folder?: string;
}
interface PresignedUrl {
uploadId: string;
url: string;
fields?: Record<string, string>; // Para POST form
expiresAt: Date;
}
```
### Generacion de URLs
```typescript
async getUploadUrl(params: UploadParams): Promise<PresignedUrl> {
const uploadId = uuid();
const key = `${this.tenantId}/${params.folder || 'files'}/${uploadId}/${params.filename}`;
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: key,
ContentType: params.mimeType,
ContentLength: params.sizeBytes,
Metadata: {
'tenant-id': this.tenantId,
'upload-id': uploadId
}
});
const url = await getSignedUrl(this.s3, command, { expiresIn: 900 });
// Guardar pending upload
await this.savePendingUpload(uploadId, params);
return { uploadId, url, expiresAt: addMinutes(new Date(), 15) };
}
```
## Limites por Plan
| Plan | Storage | Max Archivo |
|------|---------|-------------|
| Free | 100 MB | 5 MB |
| Starter | 1 GB | 25 MB |
| Pro | 10 GB | 100 MB |
| Enterprise | Ilimitado | 500 MB |
## Tipos MIME Permitidos
```typescript
const ALLOWED_MIME_TYPES = {
images: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
documents: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
spreadsheets: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
data: ['text/csv', 'application/json']
};
const BLOCKED_EXTENSIONS = ['.exe', '.bat', '.sh', '.php', '.js'];
```
## Procesamiento de Imagenes
```typescript
// Al confirmar upload de imagen
if (isImage(file.mimeType)) {
await this.imageProcessor.createThumbnails(file, [
{ name: 'thumb', width: 150, height: 150 },
{ name: 'medium', width: 800, height: 600 },
{ name: 'large', width: 1920, height: 1080 }
]);
}
```
## Estructura de Paths
```
bucket/
├── tenant-uuid-1/
│ ├── avatars/
│ │ └── user-uuid/
│ │ └── avatar.jpg
│ ├── documents/
│ │ └── upload-id/
│ │ └── contract.pdf
│ └── imports/
│ └── upload-id/
│ └── data.csv
└── tenant-uuid-2/
└── ...
```
## Entregables
| Entregable | Estado | Archivo |
|------------|--------|---------|
| storage.module.ts | Pendiente | `modules/storage/` |
| storage.service.ts | Pendiente | `services/` |
| s3.provider.ts | Pendiente | `providers/` |
| image.processor.ts | Pendiente | `services/` |
| DDL storage schema | Pendiente | `ddl/schemas/storage/` |
## Dependencias
### Depende de
- SAAS-002 (Tenants)
- SAAS-005 (Plans - limites)
- AWS S3 / Cloudflare R2 / MinIO
### Bloquea a
- Upload de avatares
- Adjuntos en modulos
- Importacion de datos
## Criterios de Aceptacion
- [ ] Upload presigned funciona
- [ ] Download presigned funciona
- [ ] Limites se respetan
- [ ] Thumbnails se generan
- [ ] Uso se trackea
- [ ] Archivos se aislan por tenant
## Configuracion
```typescript
{
storage: {
provider: 's3', // 's3' | 'r2' | 'minio'
bucket: process.env.STORAGE_BUCKET,
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
},
// Para R2
endpoint: process.env.R2_ENDPOINT,
// Para MinIO (dev)
endpoint: 'http://localhost:9000',
forcePathStyle: true
}
}
```
---
**Ultima actualizacion:** 2026-01-07