RF-USER-002: Perfil de Usuario
Identificacion
| Campo |
Valor |
| ID |
RF-USER-002 |
| Modulo |
MGN-002 |
| Nombre Modulo |
Users - Gestion de Usuarios |
| Prioridad |
P1 |
| Complejidad |
Baja |
| Estado |
Aprobado |
| Autor |
System |
| Fecha |
2025-12-05 |
Descripcion
El sistema debe permitir a cada usuario ver y editar su propio perfil, incluyendo informacion personal, foto de perfil y datos de contacto. A diferencia del CRUD de usuarios (RF-USER-001), el perfil es autoservicio - cada usuario gestiona su propia informacion.
Contexto de Negocio
El perfil de usuario permite:
- Personalizacion de la experiencia
- Informacion de contacto actualizada
- Identidad visual mediante avatar
- Datos para notificaciones y comunicacion
Criterios de Aceptacion
Ejemplos de Verificacion
Scenario: Ver perfil propio
Given un usuario autenticado
When accede a GET /api/v1/users/me
Then el sistema retorna su perfil completo
And incluye firstName, lastName, email, phone, avatarUrl
And incluye createdAt, lastLoginAt
And NO incluye passwordHash ni datos sensibles
Scenario: Actualizar nombre
Given un usuario autenticado
When actualiza su nombre a "Carlos"
Then el sistema guarda el cambio
And updated_by se establece con su propio ID
And responde con el perfil actualizado
Scenario: Subir avatar
Given un usuario autenticado
And una imagen JPG de 2MB
When sube la imagen como avatar
Then el sistema redimensiona a 200x200 px
And genera thumbnail de 50x50 px
And almacena en storage (S3/local)
And actualiza avatarUrl en el usuario
Scenario: Imagen muy grande
Given un usuario autenticado
And una imagen de 15MB
When intenta subir como avatar
Then el sistema responde con status 400
And el mensaje es "Imagen excede tamaño maximo (10MB)"
Reglas de Negocio
| ID |
Regla |
Validacion |
| RN-001 |
Solo el propio usuario edita su perfil |
user.id == request.userId |
| RN-002 |
Email no editable desde perfil |
Campo readonly |
| RN-003 |
Avatar max 10MB |
File size validation |
| RN-004 |
Formatos permitidos: JPG, PNG, WebP |
MIME type check |
| RN-005 |
Avatar redimensionado a 200x200 |
Image processing |
| RN-006 |
Telefono formato E.164 |
Regex +[0-9]{10,15} |
| RN-007 |
Nombre min 2, max 100 caracteres |
String validation |
Campos Editables vs No Editables
| Campo |
Editable |
Notas |
| firstName |
Si |
Min 2 chars |
| lastName |
Si |
Min 2 chars |
| phone |
Si |
Formato E.164 |
| avatarUrl |
Si |
Via upload |
| email |
No |
Requiere proceso separado |
| status |
No |
Solo admin |
| roles |
No |
Solo admin |
| tenantId |
No |
Inmutable |
Impacto en Capas
Database
| Elemento |
Accion |
Descripcion |
| Tabla |
usar |
users - ya existe |
| Columna |
agregar |
avatar_url VARCHAR(500) |
| Columna |
agregar |
avatar_thumbnail_url VARCHAR(500) |
| Tabla |
crear |
user_avatars - historial de avatares |
Backend
| Elemento |
Accion |
Descripcion |
| Controller |
agregar |
UsersController.getProfile() |
| Controller |
agregar |
UsersController.updateProfile() |
| Controller |
agregar |
UsersController.uploadAvatar() |
| Method |
crear |
UsersService.getProfile() |
| Method |
crear |
UsersService.updateProfile() |
| Method |
crear |
AvatarService.upload() |
| Method |
crear |
AvatarService.resize() |
| DTO |
crear |
UpdateProfileDto |
| DTO |
crear |
ProfileResponseDto |
| Endpoint |
crear |
GET /api/v1/users/me |
| Endpoint |
crear |
PATCH /api/v1/users/me |
| Endpoint |
crear |
POST /api/v1/users/me/avatar |
| Endpoint |
crear |
DELETE /api/v1/users/me/avatar |
Frontend
| Elemento |
Accion |
Descripcion |
| Pagina |
crear |
ProfilePage |
| Componente |
crear |
ProfileForm |
| Componente |
crear |
AvatarUploader |
| Componente |
crear |
AvatarCropper |
| Service |
crear |
profileService |
Dependencias
Depende de (Bloqueantes)
| ID |
Requerimiento |
Estado |
| RF-USER-001 |
CRUD Usuarios |
Tabla users |
| RF-AUTH-001 |
Login |
Autenticacion |
Dependencias Externas
| Servicio |
Descripcion |
| Storage |
S3, MinIO o filesystem para avatares |
| Image Processing |
Sharp o similar para resize |
Especificaciones Tecnicas
Endpoint GET /api/v1/users/me
// Response 200
{
"id": "uuid",
"email": "user@example.com",
"firstName": "Juan",
"lastName": "Perez",
"phone": "+521234567890",
"avatarUrl": "https://storage.erp.com/avatars/uuid-200.jpg",
"avatarThumbnailUrl": "https://storage.erp.com/avatars/uuid-50.jpg",
"status": "active",
"emailVerifiedAt": "2025-01-01T00:00:00Z",
"lastLoginAt": "2025-12-05T10:30:00Z",
"createdAt": "2025-01-01T00:00:00Z",
"tenant": {
"id": "tenant-uuid",
"name": "Empresa XYZ"
},
"roles": [
{ "id": "role-uuid", "name": "admin" }
]
}
Endpoint PATCH /api/v1/users/me
// Request
{
"firstName": "Carlos",
"lastName": "Lopez",
"phone": "+521234567890"
}
// Response 200
{
// ProfileResponseDto actualizado
}
Endpoint POST /api/v1/users/me/avatar
// Request
// Content-Type: multipart/form-data
// Field: avatar (file)
// Response 200
{
"avatarUrl": "https://storage.erp.com/avatars/uuid-200.jpg",
"avatarThumbnailUrl": "https://storage.erp.com/avatars/uuid-50.jpg"
}
Procesamiento de Avatar
// avatar.service.ts
async uploadAvatar(userId: string, file: Express.Multer.File): Promise<AvatarUrls> {
// 1. Validar archivo
this.validateFile(file); // size, mime type
// 2. Generar nombres unicos
const filename = `${userId}-${Date.now()}`;
// 3. Procesar imagen
const mainBuffer = await sharp(file.buffer)
.resize(200, 200, { fit: 'cover' })
.jpeg({ quality: 85 })
.toBuffer();
const thumbBuffer = await sharp(file.buffer)
.resize(50, 50, { fit: 'cover' })
.jpeg({ quality: 80 })
.toBuffer();
// 4. Subir a storage
const mainUrl = await this.storage.upload(`avatars/${filename}-200.jpg`, mainBuffer);
const thumbUrl = await this.storage.upload(`avatars/${filename}-50.jpg`, thumbBuffer);
// 5. Eliminar avatar anterior (opcional)
await this.deleteOldAvatar(userId);
// 6. Actualizar usuario
await this.usersRepository.update(userId, {
avatarUrl: mainUrl,
avatarThumbnailUrl: thumbUrl,
});
return { avatarUrl: mainUrl, avatarThumbnailUrl: thumbUrl };
}
Datos de Prueba
| Escenario |
Entrada |
Resultado |
| Ver perfil |
GET /users/me |
200, perfil completo |
| Actualizar nombre |
firstName: "Carlos" |
200, actualizado |
| Telefono invalido |
phone: "123" |
400, "Formato invalido" |
| Avatar JPG valido |
imagen 1MB |
200, URLs generadas |
| Avatar muy grande |
imagen 15MB |
400, "Excede limite" |
| Avatar formato invalido |
archivo .pdf |
400, "Formato no permitido" |
| Eliminar avatar |
DELETE /users/me/avatar |
200, avatar eliminado |
Estimacion
| Capa |
Story Points |
Notas |
| Database |
1 |
Columnas avatar |
| Backend |
3 |
Profile endpoints + avatar |
| Frontend |
3 |
Profile page + avatar uploader |
| Total |
7 |
|
Notas Adicionales
- Considerar CDN para servir avatares
- Implementar cache de avatares
- Avatar por defecto basado en iniciales (fallback)
- Considerar gravatar como fallback
- Rate limiting en upload de avatares
Historial de Cambios
| Version |
Fecha |
Autor |
Cambios |
| 1.0 |
2025-12-05 |
System |
Creacion inicial |
Aprobaciones
| Rol |
Nombre |
Fecha |
Firma |
| Analista |
System |
2025-12-05 |
[x] |
| Tech Lead |
- |
- |
[ ] |
| Product Owner |
- |
- |
[ ] |