[TASK-MASTER] feat: BE-001 biometrics service + BE-002 invoices deprecated
BE-001: Biometrics - Servicio minimo de solo lectura - biometrics.service.ts con metodos de consulta - biometrics.controller.ts con 10 endpoints GET - DTOs para filtros y respuestas - README actualizado BE-002: Invoices - Marcado como deprecated - DEPRECATED.md con plan de migracion - Entities marcadas con @deprecated - Mapeo a modulo financial documentado Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0f7feff3f8
commit
3bc1cbf7a3
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
Modulo de autenticacion biometrica para dispositivos moviles y web. Soporta fingerprint, Face ID, reconocimiento facial y escaneo de iris. Gestiona dispositivos registrados, credenciales biometricas (WebAuthn/FIDO2), sesiones y log de actividad.
|
Modulo de autenticacion biometrica para dispositivos moviles y web. Soporta fingerprint, Face ID, reconocimiento facial y escaneo de iris. Gestiona dispositivos registrados, credenciales biometricas (WebAuthn/FIDO2), sesiones y log de actividad.
|
||||||
|
|
||||||
|
**Arquitectura:** Este modulo implementa un **servicio de solo lectura**. La funcionalidad de escritura (crear, modificar, eliminar dispositivos y credenciales) se maneja desde el modulo `auth`.
|
||||||
|
|
||||||
## Entidades
|
## Entidades
|
||||||
|
|
||||||
| Entidad | Schema | Descripcion |
|
| Entidad | Schema | Descripcion |
|
||||||
@ -15,16 +17,49 @@ Modulo de autenticacion biometrica para dispositivos moviles y web. Soporta fing
|
|||||||
|
|
||||||
## Servicios
|
## Servicios
|
||||||
|
|
||||||
Este modulo actualmente solo define entidades. Los servicios de autenticacion biometrica deben implementarse en el modulo `auth` utilizando estas entidades.
|
### BiometricsService
|
||||||
|
|
||||||
## Endpoints
|
Servicio de solo lectura que proporciona metodos para consultar:
|
||||||
|
|
||||||
Los endpoints de autenticacion biometrica se exponen a traves del modulo `auth`:
|
- **Dispositivos:** `findDevicesByUserId`, `findDeviceById`, `findDeviceByUuid`
|
||||||
|
- **Credenciales:** `findCredentialsByUserId`, `findCredentialsByDeviceId`, `findCredentialById`
|
||||||
|
- **Enrollment:** `getEnrollmentStatus`
|
||||||
|
- **Sesiones:** `findSessionsByUserId`, `findActiveSessionsByUserId`, `countActiveSessions`
|
||||||
|
- **Actividad:** `findActivityByUserId`, `findActivityByDeviceId`, `getVerificationStats`
|
||||||
|
|
||||||
|
## Endpoints (Solo lectura)
|
||||||
|
|
||||||
|
Los endpoints exponen funcionalidad GET para consultar datos biometricos:
|
||||||
|
|
||||||
|
| Method | Path | Descripcion |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| GET | `/api/v1/biometrics/devices` | Lista dispositivos del usuario |
|
||||||
|
| GET | `/api/v1/biometrics/devices/:deviceId` | Obtiene dispositivo por ID |
|
||||||
|
| GET | `/api/v1/biometrics/credentials` | Lista credenciales biometricas |
|
||||||
|
| GET | `/api/v1/biometrics/credentials/:credentialId` | Obtiene credencial por ID |
|
||||||
|
| GET | `/api/v1/biometrics/enrollment/status` | Estado de enrollment biometrico |
|
||||||
|
| GET | `/api/v1/biometrics/sessions` | Lista sesiones del usuario |
|
||||||
|
| GET | `/api/v1/biometrics/sessions/active` | Sesiones activas |
|
||||||
|
| GET | `/api/v1/biometrics/activity` | Log de actividad |
|
||||||
|
| GET | `/api/v1/biometrics/activity/stats` | Estadisticas de verificacion |
|
||||||
|
|
||||||
|
### Query Parameters
|
||||||
|
|
||||||
|
| Endpoint | Parametros |
|
||||||
|
|----------|------------|
|
||||||
|
| `/devices` | `platform`, `isActive`, `biometricEnabled`, `page`, `limit` |
|
||||||
|
| `/credentials` | `biometricType`, `isActive`, `page`, `limit` |
|
||||||
|
| `/sessions` | `isActive`, `page`, `limit` |
|
||||||
|
| `/activity` | `activityType`, `activityStatus`, `startDate`, `endDate`, `page`, `limit` |
|
||||||
|
| `/activity/stats` | `startDate`, `endDate` |
|
||||||
|
|
||||||
|
## Endpoints de Escritura (en modulo `auth`)
|
||||||
|
|
||||||
|
La funcionalidad de escritura se expone a traves del modulo `auth`:
|
||||||
|
|
||||||
| Method | Path | Descripcion |
|
| Method | Path | Descripcion |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| POST | `/auth/devices/register` | Registra nuevo dispositivo |
|
| POST | `/auth/devices/register` | Registra nuevo dispositivo |
|
||||||
| GET | `/auth/devices` | Lista dispositivos del usuario |
|
|
||||||
| DELETE | `/auth/devices/:id` | Elimina dispositivo |
|
| DELETE | `/auth/devices/:id` | Elimina dispositivo |
|
||||||
| POST | `/auth/biometric/register` | Registra credencial biometrica |
|
| POST | `/auth/biometric/register` | Registra credencial biometrica |
|
||||||
| POST | `/auth/biometric/authenticate` | Autentica con biometrico |
|
| POST | `/auth/biometric/authenticate` | Autentica con biometrico |
|
||||||
@ -32,8 +67,9 @@ Los endpoints de autenticacion biometrica se exponen a traves del modulo `auth`:
|
|||||||
|
|
||||||
## Dependencias
|
## Dependencias
|
||||||
|
|
||||||
- `auth` - Modulo de autenticacion principal
|
- `auth` - Modulo de autenticacion principal (escritura)
|
||||||
- WebAuthn/FIDO2 library
|
- `typeorm` - ORM para acceso a datos
|
||||||
|
- WebAuthn/FIDO2 library (configurado en auth)
|
||||||
|
|
||||||
## Configuracion
|
## Configuracion
|
||||||
|
|
||||||
@ -76,3 +112,45 @@ Los endpoints de autenticacion biometrica se exponen a traves del modulo `auth`:
|
|||||||
- Lock temporal despues de N intentos fallidos
|
- Lock temporal despues de N intentos fallidos
|
||||||
- Registro de todas las actividades para auditoria
|
- Registro de todas las actividades para auditoria
|
||||||
- Soporte para multiples credenciales por dispositivo
|
- Soporte para multiples credenciales por dispositivo
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { BiometricsModule } from './modules/biometrics';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
// Inicializar modulo
|
||||||
|
const biometricsModule = new BiometricsModule({
|
||||||
|
dataSource: myDataSource,
|
||||||
|
basePath: '/biometrics',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Usar en Express app
|
||||||
|
app.use('/api/v1', authMiddleware, biometricsModule.router);
|
||||||
|
|
||||||
|
// Acceder al servicio directamente
|
||||||
|
const enrollmentStatus = await biometricsModule.biometricsService.getEnrollmentStatus(userId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Estructura del Modulo
|
||||||
|
|
||||||
|
```
|
||||||
|
biometrics/
|
||||||
|
├── biometrics.controller.ts # Controller con endpoints GET
|
||||||
|
├── biometrics.module.ts # Configuracion del modulo
|
||||||
|
├── biometrics.routes.ts # Definicion de rutas
|
||||||
|
├── index.ts # Exports publicos
|
||||||
|
├── README.md # Esta documentacion
|
||||||
|
├── dto/
|
||||||
|
│ ├── biometrics.dto.ts # DTOs de query y response
|
||||||
|
│ └── index.ts
|
||||||
|
├── entities/
|
||||||
|
│ ├── device.entity.ts
|
||||||
|
│ ├── biometric-credential.entity.ts
|
||||||
|
│ ├── device-session.entity.ts
|
||||||
|
│ ├── device-activity-log.entity.ts
|
||||||
|
│ └── index.ts
|
||||||
|
└── services/
|
||||||
|
├── biometrics.service.ts # Servicio de solo lectura
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|||||||
327
src/modules/biometrics/biometrics.controller.ts
Normal file
327
src/modules/biometrics/biometrics.controller.ts
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
import { Response, NextFunction } from 'express';
|
||||||
|
import { AuthenticatedRequest, ApiResponse, NotFoundError } from '../../shared/types/index.js';
|
||||||
|
import { BiometricsService } from './services/biometrics.service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BiometricsController - Controller de solo lectura para biometricos
|
||||||
|
*
|
||||||
|
* Endpoints GET only:
|
||||||
|
* - GET /api/v1/biometrics/devices - Listar dispositivos del usuario
|
||||||
|
* - GET /api/v1/biometrics/devices/:deviceId - Obtener dispositivo por ID
|
||||||
|
* - GET /api/v1/biometrics/credentials - Listar credenciales del usuario
|
||||||
|
* - GET /api/v1/biometrics/credentials/:credentialId - Obtener credencial por ID
|
||||||
|
* - GET /api/v1/biometrics/enrollment/status - Estado de enrollment
|
||||||
|
* - GET /api/v1/biometrics/sessions - Listar sesiones del usuario
|
||||||
|
* - GET /api/v1/biometrics/sessions/active - Sesiones activas
|
||||||
|
* - GET /api/v1/biometrics/activity - Log de actividad
|
||||||
|
* - GET /api/v1/biometrics/activity/stats - Estadisticas de verificacion
|
||||||
|
*/
|
||||||
|
export class BiometricsController {
|
||||||
|
constructor(private readonly biometricsService: BiometricsService) {}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// DEVICES
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/biometrics/devices
|
||||||
|
* Lista dispositivos del usuario autenticado
|
||||||
|
*/
|
||||||
|
async getDevices(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const { platform, isActive, biometricEnabled, page = 1, limit = 20 } = req.query;
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
platform: platform as any,
|
||||||
|
isActive: isActive === 'true' ? true : isActive === 'false' ? false : undefined,
|
||||||
|
biometricEnabled:
|
||||||
|
biometricEnabled === 'true' ? true : biometricEnabled === 'false' ? false : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.biometricsService.findDevicesByUserId(userId, filters, {
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: result.data,
|
||||||
|
meta: {
|
||||||
|
total: result.total,
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
totalPages: Math.ceil(result.total / Number(limit)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/biometrics/devices/:deviceId
|
||||||
|
* Obtiene un dispositivo por ID
|
||||||
|
*/
|
||||||
|
async getDeviceById(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const { deviceId } = req.params;
|
||||||
|
|
||||||
|
const device = await this.biometricsService.findDeviceById(deviceId, userId);
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
throw new NotFoundError('Dispositivo no encontrado');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: device,
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CREDENTIALS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/biometrics/credentials
|
||||||
|
* Lista credenciales biometricas del usuario
|
||||||
|
*/
|
||||||
|
async getCredentials(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const { biometricType, isActive, page = 1, limit = 20 } = req.query;
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
biometricType: biometricType as any,
|
||||||
|
isActive: isActive === 'true' ? true : isActive === 'false' ? false : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.biometricsService.findCredentialsByUserId(userId, filters, {
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: result.data,
|
||||||
|
meta: {
|
||||||
|
total: result.total,
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
totalPages: Math.ceil(result.total / Number(limit)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/biometrics/credentials/:credentialId
|
||||||
|
* Obtiene una credencial por ID
|
||||||
|
*/
|
||||||
|
async getCredentialById(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const { credentialId } = req.params;
|
||||||
|
|
||||||
|
const credential = await this.biometricsService.findCredentialById(credentialId, userId);
|
||||||
|
|
||||||
|
if (!credential) {
|
||||||
|
throw new NotFoundError('Credencial biometrica no encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: credential,
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ENROLLMENT STATUS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/biometrics/enrollment/status
|
||||||
|
* Obtiene el estado de enrollment biometrico del usuario
|
||||||
|
*/
|
||||||
|
async getEnrollmentStatus(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
|
||||||
|
const status = await this.biometricsService.getEnrollmentStatus(userId);
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: status,
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SESSIONS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/biometrics/sessions
|
||||||
|
* Lista sesiones del usuario
|
||||||
|
*/
|
||||||
|
async getSessions(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const { isActive, page = 1, limit = 20 } = req.query;
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
isActive: isActive === 'true' ? true : isActive === 'false' ? false : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.biometricsService.findSessionsByUserId(userId, filters, {
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: result.data,
|
||||||
|
meta: {
|
||||||
|
total: result.total,
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
totalPages: Math.ceil(result.total / Number(limit)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/biometrics/sessions/active
|
||||||
|
* Lista sesiones activas del usuario
|
||||||
|
*/
|
||||||
|
async getActiveSessions(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
|
||||||
|
const sessions = await this.biometricsService.findActiveSessionsByUserId(userId);
|
||||||
|
const count = await this.biometricsService.countActiveSessions(userId);
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
sessions,
|
||||||
|
count,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ACTIVITY LOG
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/biometrics/activity
|
||||||
|
* Lista actividad del usuario
|
||||||
|
*/
|
||||||
|
async getActivity(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const { activityType, activityStatus, startDate, endDate, page = 1, limit = 50 } = req.query;
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
activityType: activityType as any,
|
||||||
|
activityStatus: activityStatus as any,
|
||||||
|
startDate: startDate ? new Date(startDate as string) : undefined,
|
||||||
|
endDate: endDate ? new Date(endDate as string) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.biometricsService.findActivityByUserId(userId, filters, {
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: result.data,
|
||||||
|
meta: {
|
||||||
|
total: result.total,
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
totalPages: Math.ceil(result.total / Number(limit)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/biometrics/activity/stats
|
||||||
|
* Estadisticas de verificaciones biometricas
|
||||||
|
*/
|
||||||
|
async getVerificationStats(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const { startDate, endDate } = req.query;
|
||||||
|
|
||||||
|
const stats = await this.biometricsService.getVerificationStats(
|
||||||
|
userId,
|
||||||
|
startDate ? new Date(startDate as string) : undefined,
|
||||||
|
endDate ? new Date(endDate as string) : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: stats,
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/modules/biometrics/biometrics.module.ts
Normal file
70
src/modules/biometrics/biometrics.module.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { BiometricsService } from './services/biometrics.service.js';
|
||||||
|
import { BiometricsController } from './biometrics.controller.js';
|
||||||
|
import { createBiometricsRoutes } from './biometrics.routes.js';
|
||||||
|
import {
|
||||||
|
Device,
|
||||||
|
BiometricCredential,
|
||||||
|
DeviceSession,
|
||||||
|
DeviceActivityLog,
|
||||||
|
} from './entities/index.js';
|
||||||
|
|
||||||
|
export interface BiometricsModuleOptions {
|
||||||
|
dataSource: DataSource;
|
||||||
|
basePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BiometricsModule - Modulo de solo lectura para datos biometricos
|
||||||
|
*
|
||||||
|
* Este modulo proporciona endpoints GET para consultar:
|
||||||
|
* - Dispositivos registrados
|
||||||
|
* - Credenciales biometricas
|
||||||
|
* - Estado de enrollment
|
||||||
|
* - Sesiones activas
|
||||||
|
* - Log de actividad
|
||||||
|
*
|
||||||
|
* La funcionalidad de escritura se maneja desde el modulo `auth`.
|
||||||
|
*/
|
||||||
|
export class BiometricsModule {
|
||||||
|
public router: Router;
|
||||||
|
public biometricsService: BiometricsService;
|
||||||
|
private dataSource: DataSource;
|
||||||
|
private basePath: string;
|
||||||
|
|
||||||
|
constructor(options: BiometricsModuleOptions) {
|
||||||
|
this.dataSource = options.dataSource;
|
||||||
|
this.basePath = options.basePath || '/biometrics';
|
||||||
|
this.router = Router();
|
||||||
|
this.initializeServices();
|
||||||
|
this.initializeRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeServices(): void {
|
||||||
|
const deviceRepository = this.dataSource.getRepository(Device);
|
||||||
|
const credentialRepository = this.dataSource.getRepository(BiometricCredential);
|
||||||
|
const sessionRepository = this.dataSource.getRepository(DeviceSession);
|
||||||
|
const activityLogRepository = this.dataSource.getRepository(DeviceActivityLog);
|
||||||
|
|
||||||
|
this.biometricsService = new BiometricsService(
|
||||||
|
deviceRepository,
|
||||||
|
credentialRepository,
|
||||||
|
sessionRepository,
|
||||||
|
activityLogRepository
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeRoutes(): void {
|
||||||
|
const biometricsController = new BiometricsController(this.biometricsService);
|
||||||
|
const routes = createBiometricsRoutes(biometricsController);
|
||||||
|
this.router.use(this.basePath, routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna las entidades del modulo para registrar en TypeORM
|
||||||
|
*/
|
||||||
|
static getEntities(): Function[] {
|
||||||
|
return [Device, BiometricCredential, DeviceSession, DeviceActivityLog];
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/modules/biometrics/biometrics.routes.ts
Normal file
41
src/modules/biometrics/biometrics.routes.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { BiometricsController } from './biometrics.controller.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea las rutas del modulo biometrics
|
||||||
|
*
|
||||||
|
* Todas las rutas son GET (solo lectura):
|
||||||
|
* - GET /devices - Listar dispositivos
|
||||||
|
* - GET /devices/:deviceId - Obtener dispositivo
|
||||||
|
* - GET /credentials - Listar credenciales
|
||||||
|
* - GET /credentials/:credentialId - Obtener credencial
|
||||||
|
* - GET /enrollment/status - Estado de enrollment
|
||||||
|
* - GET /sessions - Listar sesiones
|
||||||
|
* - GET /sessions/active - Sesiones activas
|
||||||
|
* - GET /activity - Log de actividad
|
||||||
|
* - GET /activity/stats - Estadisticas
|
||||||
|
*/
|
||||||
|
export function createBiometricsRoutes(controller: BiometricsController): Router {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Devices
|
||||||
|
router.get('/devices', controller.getDevices.bind(controller));
|
||||||
|
router.get('/devices/:deviceId', controller.getDeviceById.bind(controller));
|
||||||
|
|
||||||
|
// Credentials
|
||||||
|
router.get('/credentials', controller.getCredentials.bind(controller));
|
||||||
|
router.get('/credentials/:credentialId', controller.getCredentialById.bind(controller));
|
||||||
|
|
||||||
|
// Enrollment
|
||||||
|
router.get('/enrollment/status', controller.getEnrollmentStatus.bind(controller));
|
||||||
|
|
||||||
|
// Sessions
|
||||||
|
router.get('/sessions', controller.getSessions.bind(controller));
|
||||||
|
router.get('/sessions/active', controller.getActiveSessions.bind(controller));
|
||||||
|
|
||||||
|
// Activity
|
||||||
|
router.get('/activity', controller.getActivity.bind(controller));
|
||||||
|
router.get('/activity/stats', controller.getVerificationStats.bind(controller));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
116
src/modules/biometrics/dto/biometrics.dto.ts
Normal file
116
src/modules/biometrics/dto/biometrics.dto.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { BiometricType, DevicePlatform, ActivityStatus, ActivityType } from '../entities';
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// QUERY INTERFACES (Solo lectura)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface GetDeviceQueryDto {
|
||||||
|
tenantId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDevicesQueryDto {
|
||||||
|
platform?: DevicePlatform;
|
||||||
|
isActive?: string;
|
||||||
|
biometricEnabled?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetCredentialsQueryDto {
|
||||||
|
biometricType?: BiometricType;
|
||||||
|
isActive?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetActivityLogQueryDto {
|
||||||
|
activityType?: ActivityType;
|
||||||
|
activityStatus?: ActivityStatus;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSessionsQueryDto {
|
||||||
|
isActive?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// RESPONSE INTERFACES
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface DeviceResponseDto {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
tenantId: string;
|
||||||
|
deviceUuid: string;
|
||||||
|
deviceName: string;
|
||||||
|
deviceModel: string;
|
||||||
|
deviceBrand: string;
|
||||||
|
platform: DevicePlatform;
|
||||||
|
platformVersion: string;
|
||||||
|
appVersion: string;
|
||||||
|
isActive: boolean;
|
||||||
|
isTrusted: boolean;
|
||||||
|
trustLevel: number;
|
||||||
|
biometricEnabled: boolean;
|
||||||
|
biometricType: BiometricType;
|
||||||
|
lastSeenAt: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BiometricCredentialResponseDto {
|
||||||
|
id: string;
|
||||||
|
deviceId: string;
|
||||||
|
userId: string;
|
||||||
|
biometricType: BiometricType;
|
||||||
|
credentialName: string;
|
||||||
|
isPrimary: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
lastUsedAt: Date;
|
||||||
|
useCount: number;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceSessionResponseDto {
|
||||||
|
id: string;
|
||||||
|
deviceId: string;
|
||||||
|
userId: string;
|
||||||
|
tenantId: string;
|
||||||
|
authMethod: string;
|
||||||
|
issuedAt: Date;
|
||||||
|
expiresAt: Date;
|
||||||
|
isActive: boolean;
|
||||||
|
ipAddress: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceActivityLogResponseDto {
|
||||||
|
id: string;
|
||||||
|
deviceId: string;
|
||||||
|
userId: string;
|
||||||
|
activityType: ActivityType;
|
||||||
|
activityStatus: ActivityStatus;
|
||||||
|
details: Record<string, unknown>;
|
||||||
|
ipAddress: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnrollmentStatusResponseDto {
|
||||||
|
userId: string;
|
||||||
|
hasEnrollment: boolean;
|
||||||
|
totalCredentials: number;
|
||||||
|
activeCredentials: number;
|
||||||
|
primaryCredential: BiometricCredentialResponseDto | null;
|
||||||
|
biometricTypes: BiometricType[];
|
||||||
|
devices: {
|
||||||
|
deviceId: string;
|
||||||
|
deviceName: string;
|
||||||
|
credentialsCount: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
12
src/modules/biometrics/dto/index.ts
Normal file
12
src/modules/biometrics/dto/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export {
|
||||||
|
GetDeviceQueryDto,
|
||||||
|
GetDevicesQueryDto,
|
||||||
|
GetCredentialsQueryDto,
|
||||||
|
GetActivityLogQueryDto,
|
||||||
|
GetSessionsQueryDto,
|
||||||
|
DeviceResponseDto,
|
||||||
|
BiometricCredentialResponseDto,
|
||||||
|
DeviceSessionResponseDto,
|
||||||
|
DeviceActivityLogResponseDto,
|
||||||
|
EnrollmentStatusResponseDto,
|
||||||
|
} from './biometrics.dto';
|
||||||
46
src/modules/biometrics/index.ts
Normal file
46
src/modules/biometrics/index.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Module
|
||||||
|
export { BiometricsModule, BiometricsModuleOptions } from './biometrics.module';
|
||||||
|
|
||||||
|
// Entities
|
||||||
|
export {
|
||||||
|
Device,
|
||||||
|
DevicePlatform,
|
||||||
|
BiometricType,
|
||||||
|
BiometricCredential,
|
||||||
|
DeviceSession,
|
||||||
|
AuthMethod,
|
||||||
|
DeviceActivityLog,
|
||||||
|
ActivityType,
|
||||||
|
ActivityStatus,
|
||||||
|
} from './entities';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
export {
|
||||||
|
BiometricsService,
|
||||||
|
DeviceFilters,
|
||||||
|
CredentialFilters,
|
||||||
|
ActivityLogFilters,
|
||||||
|
SessionFilters,
|
||||||
|
PaginationOptions,
|
||||||
|
EnrollmentStatus,
|
||||||
|
} from './services';
|
||||||
|
|
||||||
|
// DTOs
|
||||||
|
export {
|
||||||
|
GetDeviceQueryDto,
|
||||||
|
GetDevicesQueryDto,
|
||||||
|
GetCredentialsQueryDto,
|
||||||
|
GetActivityLogQueryDto,
|
||||||
|
GetSessionsQueryDto,
|
||||||
|
DeviceResponseDto,
|
||||||
|
BiometricCredentialResponseDto,
|
||||||
|
DeviceSessionResponseDto,
|
||||||
|
DeviceActivityLogResponseDto,
|
||||||
|
EnrollmentStatusResponseDto,
|
||||||
|
} from './dto';
|
||||||
|
|
||||||
|
// Controller
|
||||||
|
export { BiometricsController } from './biometrics.controller';
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
export { createBiometricsRoutes } from './biometrics.routes';
|
||||||
410
src/modules/biometrics/services/biometrics.service.ts
Normal file
410
src/modules/biometrics/services/biometrics.service.ts
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
import { Repository, FindOptionsWhere, Between, LessThanOrEqual, MoreThanOrEqual, In } from 'typeorm';
|
||||||
|
import {
|
||||||
|
Device,
|
||||||
|
BiometricCredential,
|
||||||
|
DeviceSession,
|
||||||
|
DeviceActivityLog,
|
||||||
|
BiometricType,
|
||||||
|
DevicePlatform,
|
||||||
|
ActivityType,
|
||||||
|
ActivityStatus,
|
||||||
|
} from '../entities';
|
||||||
|
|
||||||
|
export interface DeviceFilters {
|
||||||
|
platform?: DevicePlatform;
|
||||||
|
isActive?: boolean;
|
||||||
|
biometricEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CredentialFilters {
|
||||||
|
biometricType?: BiometricType;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActivityLogFilters {
|
||||||
|
activityType?: ActivityType;
|
||||||
|
activityStatus?: ActivityStatus;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionFilters {
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationOptions {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnrollmentStatus {
|
||||||
|
userId: string;
|
||||||
|
hasEnrollment: boolean;
|
||||||
|
totalCredentials: number;
|
||||||
|
activeCredentials: number;
|
||||||
|
primaryCredential: BiometricCredential | null;
|
||||||
|
biometricTypes: BiometricType[];
|
||||||
|
devices: {
|
||||||
|
deviceId: string;
|
||||||
|
deviceName: string;
|
||||||
|
credentialsCount: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BiometricsService - Servicio de solo lectura para datos biometricos
|
||||||
|
*
|
||||||
|
* Este servicio proporciona metodos de consulta para dispositivos,
|
||||||
|
* credenciales biometricas, sesiones y logs de actividad.
|
||||||
|
*
|
||||||
|
* La funcionalidad de escritura (crear, modificar, eliminar) se maneja
|
||||||
|
* desde los modulos `auth` y `devices`.
|
||||||
|
*/
|
||||||
|
export class BiometricsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deviceRepository: Repository<Device>,
|
||||||
|
private readonly credentialRepository: Repository<BiometricCredential>,
|
||||||
|
private readonly sessionRepository: Repository<DeviceSession>,
|
||||||
|
private readonly activityLogRepository: Repository<DeviceActivityLog>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// DEVICES (Solo lectura)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene dispositivos de un usuario
|
||||||
|
*/
|
||||||
|
async findDevicesByUserId(
|
||||||
|
userId: string,
|
||||||
|
filters: DeviceFilters = {},
|
||||||
|
pagination: PaginationOptions = {}
|
||||||
|
): Promise<{ data: Device[]; total: number }> {
|
||||||
|
const { page = 1, limit = 20 } = pagination;
|
||||||
|
const where: FindOptionsWhere<Device> = { userId };
|
||||||
|
|
||||||
|
if (filters.platform) where.platform = filters.platform;
|
||||||
|
if (filters.isActive !== undefined) where.isActive = filters.isActive;
|
||||||
|
if (filters.biometricEnabled !== undefined) where.biometricEnabled = filters.biometricEnabled;
|
||||||
|
|
||||||
|
const [data, total] = await this.deviceRepository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { lastSeenAt: 'DESC' },
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene un dispositivo por ID
|
||||||
|
*/
|
||||||
|
async findDeviceById(deviceId: string, userId?: string): Promise<Device | null> {
|
||||||
|
const where: FindOptionsWhere<Device> = { id: deviceId };
|
||||||
|
if (userId) where.userId = userId;
|
||||||
|
|
||||||
|
return this.deviceRepository.findOne({
|
||||||
|
where,
|
||||||
|
relations: ['biometricCredentials', 'sessions'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene un dispositivo por UUID
|
||||||
|
*/
|
||||||
|
async findDeviceByUuid(deviceUuid: string, userId: string): Promise<Device | null> {
|
||||||
|
return this.deviceRepository.findOne({
|
||||||
|
where: { deviceUuid, userId },
|
||||||
|
relations: ['biometricCredentials'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// BIOMETRIC CREDENTIALS (Solo lectura)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene credenciales biometricas de un usuario
|
||||||
|
*/
|
||||||
|
async findCredentialsByUserId(
|
||||||
|
userId: string,
|
||||||
|
filters: CredentialFilters = {},
|
||||||
|
pagination: PaginationOptions = {}
|
||||||
|
): Promise<{ data: BiometricCredential[]; total: number }> {
|
||||||
|
const { page = 1, limit = 20 } = pagination;
|
||||||
|
const where: FindOptionsWhere<BiometricCredential> = { userId };
|
||||||
|
|
||||||
|
if (filters.biometricType) where.biometricType = filters.biometricType;
|
||||||
|
if (filters.isActive !== undefined) where.isActive = filters.isActive;
|
||||||
|
|
||||||
|
const [data, total] = await this.credentialRepository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
relations: ['device'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene credenciales de un dispositivo
|
||||||
|
*/
|
||||||
|
async findCredentialsByDeviceId(
|
||||||
|
deviceId: string,
|
||||||
|
userId?: string
|
||||||
|
): Promise<BiometricCredential[]> {
|
||||||
|
const where: FindOptionsWhere<BiometricCredential> = { deviceId };
|
||||||
|
if (userId) where.userId = userId;
|
||||||
|
|
||||||
|
return this.credentialRepository.find({
|
||||||
|
where,
|
||||||
|
order: { isPrimary: 'DESC', createdAt: 'DESC' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene una credencial por ID
|
||||||
|
*/
|
||||||
|
async findCredentialById(
|
||||||
|
credentialId: string,
|
||||||
|
userId?: string
|
||||||
|
): Promise<BiometricCredential | null> {
|
||||||
|
const where: FindOptionsWhere<BiometricCredential> = { id: credentialId };
|
||||||
|
if (userId) where.userId = userId;
|
||||||
|
|
||||||
|
return this.credentialRepository.findOne({
|
||||||
|
where,
|
||||||
|
relations: ['device'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ENROLLMENT STATUS (Solo lectura)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica el estado de enrollment biometrico de un usuario
|
||||||
|
*/
|
||||||
|
async getEnrollmentStatus(userId: string): Promise<EnrollmentStatus> {
|
||||||
|
const credentials = await this.credentialRepository.find({
|
||||||
|
where: { userId },
|
||||||
|
relations: ['device'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const devices = await this.deviceRepository.find({
|
||||||
|
where: { userId, biometricEnabled: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeCredentials = credentials.filter((c) => c.isActive);
|
||||||
|
const primaryCredential = credentials.find((c) => c.isPrimary && c.isActive) || null;
|
||||||
|
|
||||||
|
// Obtener tipos biometricos unicos
|
||||||
|
const biometricTypes = [...new Set(activeCredentials.map((c) => c.biometricType))];
|
||||||
|
|
||||||
|
// Agrupar credenciales por dispositivo
|
||||||
|
const deviceCredentialsMap = new Map<string, { name: string; count: number }>();
|
||||||
|
for (const credential of activeCredentials) {
|
||||||
|
const device = credential.device;
|
||||||
|
if (device) {
|
||||||
|
const existing = deviceCredentialsMap.get(device.id);
|
||||||
|
if (existing) {
|
||||||
|
existing.count++;
|
||||||
|
} else {
|
||||||
|
deviceCredentialsMap.set(device.id, {
|
||||||
|
name: device.deviceName || device.deviceModel || 'Dispositivo desconocido',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const devicesList = Array.from(deviceCredentialsMap.entries()).map(([id, data]) => ({
|
||||||
|
deviceId: id,
|
||||||
|
deviceName: data.name,
|
||||||
|
credentialsCount: data.count,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
hasEnrollment: activeCredentials.length > 0,
|
||||||
|
totalCredentials: credentials.length,
|
||||||
|
activeCredentials: activeCredentials.length,
|
||||||
|
primaryCredential,
|
||||||
|
biometricTypes,
|
||||||
|
devices: devicesList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// DEVICE SESSIONS (Solo lectura)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista sesiones de un usuario
|
||||||
|
*/
|
||||||
|
async findSessionsByUserId(
|
||||||
|
userId: string,
|
||||||
|
filters: SessionFilters = {},
|
||||||
|
pagination: PaginationOptions = {}
|
||||||
|
): Promise<{ data: DeviceSession[]; total: number }> {
|
||||||
|
const { page = 1, limit = 20 } = pagination;
|
||||||
|
const where: FindOptionsWhere<DeviceSession> = { userId };
|
||||||
|
|
||||||
|
if (filters.isActive !== undefined) where.isActive = filters.isActive;
|
||||||
|
|
||||||
|
const [data, total] = await this.sessionRepository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { issuedAt: 'DESC' },
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
relations: ['device'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene sesiones activas de un usuario
|
||||||
|
*/
|
||||||
|
async findActiveSessionsByUserId(userId: string): Promise<DeviceSession[]> {
|
||||||
|
const now = new Date();
|
||||||
|
return this.sessionRepository.find({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
isActive: true,
|
||||||
|
expiresAt: MoreThanOrEqual(now),
|
||||||
|
},
|
||||||
|
order: { issuedAt: 'DESC' },
|
||||||
|
relations: ['device'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cuenta sesiones activas de un usuario
|
||||||
|
*/
|
||||||
|
async countActiveSessions(userId: string): Promise<number> {
|
||||||
|
const now = new Date();
|
||||||
|
return this.sessionRepository.count({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
isActive: true,
|
||||||
|
expiresAt: MoreThanOrEqual(now),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ACTIVITY LOG (Solo lectura)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista actividad de un usuario
|
||||||
|
*/
|
||||||
|
async findActivityByUserId(
|
||||||
|
userId: string,
|
||||||
|
filters: ActivityLogFilters = {},
|
||||||
|
pagination: PaginationOptions = {}
|
||||||
|
): Promise<{ data: DeviceActivityLog[]; total: number }> {
|
||||||
|
const { page = 1, limit = 50 } = pagination;
|
||||||
|
const where: FindOptionsWhere<DeviceActivityLog> = { userId };
|
||||||
|
|
||||||
|
if (filters.activityType) where.activityType = filters.activityType;
|
||||||
|
if (filters.activityStatus) where.activityStatus = filters.activityStatus;
|
||||||
|
|
||||||
|
if (filters.startDate && filters.endDate) {
|
||||||
|
where.createdAt = Between(filters.startDate, filters.endDate);
|
||||||
|
} else if (filters.startDate) {
|
||||||
|
where.createdAt = MoreThanOrEqual(filters.startDate);
|
||||||
|
} else if (filters.endDate) {
|
||||||
|
where.createdAt = LessThanOrEqual(filters.endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await this.activityLogRepository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista actividad de un dispositivo
|
||||||
|
*/
|
||||||
|
async findActivityByDeviceId(
|
||||||
|
deviceId: string,
|
||||||
|
filters: ActivityLogFilters = {},
|
||||||
|
pagination: PaginationOptions = {}
|
||||||
|
): Promise<{ data: DeviceActivityLog[]; total: number }> {
|
||||||
|
const { page = 1, limit = 50 } = pagination;
|
||||||
|
const where: FindOptionsWhere<DeviceActivityLog> = { deviceId };
|
||||||
|
|
||||||
|
if (filters.activityType) where.activityType = filters.activityType;
|
||||||
|
if (filters.activityStatus) where.activityStatus = filters.activityStatus;
|
||||||
|
|
||||||
|
if (filters.startDate && filters.endDate) {
|
||||||
|
where.createdAt = Between(filters.startDate, filters.endDate);
|
||||||
|
} else if (filters.startDate) {
|
||||||
|
where.createdAt = MoreThanOrEqual(filters.startDate);
|
||||||
|
} else if (filters.endDate) {
|
||||||
|
where.createdAt = LessThanOrEqual(filters.endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await this.activityLogRepository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene estadisticas de verificaciones biometricas
|
||||||
|
*/
|
||||||
|
async getVerificationStats(
|
||||||
|
userId: string,
|
||||||
|
startDate?: Date,
|
||||||
|
endDate?: Date
|
||||||
|
): Promise<{
|
||||||
|
total: number;
|
||||||
|
successful: number;
|
||||||
|
failed: number;
|
||||||
|
blocked: number;
|
||||||
|
successRate: number;
|
||||||
|
}> {
|
||||||
|
const where: FindOptionsWhere<DeviceActivityLog> = {
|
||||||
|
userId,
|
||||||
|
activityType: 'biometric_auth',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
where.createdAt = Between(startDate, endDate);
|
||||||
|
} else if (startDate) {
|
||||||
|
where.createdAt = MoreThanOrEqual(startDate);
|
||||||
|
} else if (endDate) {
|
||||||
|
where.createdAt = LessThanOrEqual(endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logs = await this.activityLogRepository.find({ where });
|
||||||
|
|
||||||
|
const total = logs.length;
|
||||||
|
const successful = logs.filter((l) => l.activityStatus === 'success').length;
|
||||||
|
const failed = logs.filter((l) => l.activityStatus === 'failed').length;
|
||||||
|
const blocked = logs.filter((l) => l.activityStatus === 'blocked').length;
|
||||||
|
const successRate = total > 0 ? (successful / total) * 100 : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
successful,
|
||||||
|
failed,
|
||||||
|
blocked,
|
||||||
|
successRate: Math.round(successRate * 100) / 100,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/modules/biometrics/services/index.ts
Normal file
9
src/modules/biometrics/services/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export {
|
||||||
|
BiometricsService,
|
||||||
|
DeviceFilters,
|
||||||
|
CredentialFilters,
|
||||||
|
ActivityLogFilters,
|
||||||
|
SessionFilters,
|
||||||
|
PaginationOptions,
|
||||||
|
EnrollmentStatus,
|
||||||
|
} from './biometrics.service';
|
||||||
67
src/modules/invoices/DEPRECATED.md
Normal file
67
src/modules/invoices/DEPRECATED.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# MODULO DEPRECATED
|
||||||
|
|
||||||
|
Este modulo esta deprecated desde 2026-02-03.
|
||||||
|
|
||||||
|
## Razon
|
||||||
|
|
||||||
|
La funcionalidad de facturacion esta siendo consolidada en el modulo `financial`.
|
||||||
|
El modulo `invoices` (schema `billing`) fue disenado para facturacion operativa,
|
||||||
|
pero para mantener una arquitectura limpia, toda la logica de facturas debe
|
||||||
|
residir en un unico modulo.
|
||||||
|
|
||||||
|
## Mapeo de Entidades
|
||||||
|
|
||||||
|
| Invoices (billing) | Financial (financial) | Notas |
|
||||||
|
|------------------------|------------------------------|------------------------------------------|
|
||||||
|
| Invoice | financial/invoice.entity.ts | financial tiene integracion con journals |
|
||||||
|
| InvoiceItem | financial/invoice-line.entity.ts | Renombrado a InvoiceLine |
|
||||||
|
| Payment | financial/payment.entity.ts | financial tiene PaymentType enum |
|
||||||
|
| PaymentAllocation | (sin equivalente directo) | Usar payment_invoices table |
|
||||||
|
|
||||||
|
## Diferencias Clave
|
||||||
|
|
||||||
|
### Invoices (deprecated)
|
||||||
|
- Schema: `billing`
|
||||||
|
- Enfoque: Facturacion operativa, CFDI Mexico, SaaS billing
|
||||||
|
- Sin integracion contable directa
|
||||||
|
|
||||||
|
### Financial (activo)
|
||||||
|
- Schema: `financial`
|
||||||
|
- Enfoque: Contabilidad completa con journal entries
|
||||||
|
- Integracion con plan de cuentas
|
||||||
|
- Soporte para asientos automaticos
|
||||||
|
|
||||||
|
## Migracion
|
||||||
|
|
||||||
|
Para migrar codigo existente:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ANTES (deprecated)
|
||||||
|
import { Invoice, Payment } from '@modules/invoices';
|
||||||
|
|
||||||
|
// DESPUES (recomendado)
|
||||||
|
import { Invoice, Payment } from '@modules/financial/entities';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uso Actual
|
||||||
|
|
||||||
|
Este modulo aun tiene las siguientes dependencias activas:
|
||||||
|
|
||||||
|
1. `app.integration.ts` - Importa InvoicesModule y entities
|
||||||
|
2. `app.ts` - Importa invoicesRoutes
|
||||||
|
3. `billing-usage/entities/invoice.entity.ts` - Referencia en comentario deprecated
|
||||||
|
|
||||||
|
**IMPORTANTE:** Estas referencias deben actualizarse antes de eliminar este modulo.
|
||||||
|
|
||||||
|
## Plan de Deprecation
|
||||||
|
|
||||||
|
1. **Fase 1 (Actual):** Marcar como deprecated, documentar
|
||||||
|
2. **Fase 2:** Migrar endpoints a financial module
|
||||||
|
3. **Fase 3:** Actualizar todas las referencias
|
||||||
|
4. **Fase 4:** Eliminar modulo invoices
|
||||||
|
|
||||||
|
## Contacto
|
||||||
|
|
||||||
|
Para dudas sobre la migracion, consultar la documentacion en:
|
||||||
|
- `orchestration/tareas/2026-02-03/` - Tarea de consolidacion
|
||||||
|
- `docs/40-estandares/` - Estandares de arquitectura
|
||||||
@ -1,6 +1,12 @@
|
|||||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
import { Invoice } from './invoice.entity';
|
import { Invoice } from './invoice.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Item Entity
|
||||||
|
*
|
||||||
|
* @deprecated Since 2026-02-03. Use financial/invoice-line.entity.ts instead.
|
||||||
|
* @see InvoiceLine from '@modules/financial/entities'
|
||||||
|
*/
|
||||||
@Entity({ name: 'invoice_items', schema: 'billing' })
|
@Entity({ name: 'invoice_items', schema: 'billing' })
|
||||||
export class InvoiceItem {
|
export class InvoiceItem {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
@ -19,6 +19,9 @@ import { InvoiceItem } from './invoice-item.entity';
|
|||||||
* Context discriminator:
|
* Context discriminator:
|
||||||
* - 'commercial': Sales/purchase invoices (salesOrderId, purchaseOrderId, partnerId)
|
* - 'commercial': Sales/purchase invoices (salesOrderId, purchaseOrderId, partnerId)
|
||||||
* - 'saas': SaaS subscription invoices (subscriptionId, periodStart, periodEnd)
|
* - 'saas': SaaS subscription invoices (subscriptionId, periodStart, periodEnd)
|
||||||
|
*
|
||||||
|
* @deprecated Since 2026-02-03. Use financial/invoice.entity.ts instead.
|
||||||
|
* @see Invoice from '@modules/financial/entities'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type InvoiceType = 'sale' | 'purchase' | 'credit_note' | 'debit_note';
|
export type InvoiceType = 'sale' | 'purchase' | 'credit_note' | 'debit_note';
|
||||||
|
|||||||
@ -2,6 +2,14 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index, ManyTo
|
|||||||
import { Payment } from './payment.entity';
|
import { Payment } from './payment.entity';
|
||||||
import { Invoice } from './invoice.entity';
|
import { Invoice } from './invoice.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payment Allocation Entity
|
||||||
|
*
|
||||||
|
* Maps payments to invoices for partial payment tracking.
|
||||||
|
*
|
||||||
|
* @deprecated Since 2026-02-03. Use payment_invoices relationship in financial module.
|
||||||
|
* @see Payment from '@modules/financial/entities'
|
||||||
|
*/
|
||||||
@Entity({ name: 'payment_allocations', schema: 'billing' })
|
@Entity({ name: 'payment_allocations', schema: 'billing' })
|
||||||
export class PaymentAllocation {
|
export class PaymentAllocation {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, Index } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, Index } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payment Entity
|
||||||
|
*
|
||||||
|
* @deprecated Since 2026-02-03. Use financial/payment.entity.ts instead.
|
||||||
|
* @see Payment from '@modules/financial/entities'
|
||||||
|
*/
|
||||||
@Entity({ name: 'payments', schema: 'billing' })
|
@Entity({ name: 'payments', schema: 'billing' })
|
||||||
export class Payment {
|
export class Payment {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user