# 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 { // 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 | - | - | [ ] |