315 lines
8.4 KiB
Markdown
315 lines
8.4 KiB
Markdown
# 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
|
|
|
|
- [x] **CA-001:** El usuario debe poder ver su perfil completo
|
|
- [x] **CA-002:** El usuario debe poder editar nombre y apellido
|
|
- [x] **CA-003:** El usuario debe poder editar telefono
|
|
- [x] **CA-004:** El usuario debe poder subir foto de perfil (avatar)
|
|
- [x] **CA-005:** El usuario NO debe poder cambiar su email directamente
|
|
- [x] **CA-006:** El sistema debe validar formato de telefono
|
|
- [x] **CA-007:** El sistema debe redimensionar imagenes de avatar automaticamente
|
|
- [x] **CA-008:** El perfil debe mostrar informacion de la cuenta (fecha registro, ultimo login)
|
|
|
|
### Ejemplos de Verificacion
|
|
|
|
```gherkin
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// Request
|
|
{
|
|
"firstName": "Carlos",
|
|
"lastName": "Lopez",
|
|
"phone": "+521234567890"
|
|
}
|
|
|
|
// Response 200
|
|
{
|
|
// ProfileResponseDto actualizado
|
|
}
|
|
```
|
|
|
|
### Endpoint POST /api/v1/users/me/avatar
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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 | - | - | [ ] |
|