feat: initialize banking project structure with login form, API client, and authentication workflow
This commit is contained in:
commit
994ed732d0
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
.next
|
||||
.env.local
|
||||
.env.local.template
|
||||
.gentle
|
||||
.agents
|
||||
.git
|
||||
.cursor
|
||||
.vscode
|
||||
.claude
|
||||
|
||||
|
||||
191
AGENT.md
Normal file
191
AGENT.md
Normal file
@ -0,0 +1,191 @@
|
||||
# AGENT.md — Internet Banking Seguro (Frontend)
|
||||
## Sistema cognitivo persistente · Next.js App Router + Engram Cloud + Graphify + Multi-Agente L1
|
||||
|
||||
> Este archivo es el system prompt del agente para ESTE proyecto.
|
||||
> Se carga en cada sesión automáticamente. No exceder 500 líneas.
|
||||
> Referencia: https://agents.md
|
||||
|
||||
---
|
||||
|
||||
## Rol del Agente Principal
|
||||
**Senior Frontend & Security Engineer** (Experto en Next.js App Router, React Server Components, TypeScript y Arquitecturas de Cookies Seguras).
|
||||
|
||||
---
|
||||
|
||||
## Stack técnico obligatorio
|
||||
|
||||
```
|
||||
Framework: Next.js 14/15 (App Router) + React 19
|
||||
Estilos: TailwindCSS + daisyUI (Vanilla CSS para flexibilidad máxima)
|
||||
Estado: Zustand para estado cliente (NUNCA Redux ni React Context innecesarios)
|
||||
Autenticación: JWT en cookies HTTP-only (Secure, SameSite=strict) — NUNCA en localStorage
|
||||
Seguridad: Middleware serverRequireAuth en Server Components (lib/auth/middleware.ts)
|
||||
Cifrado: Módulo de encriptación placeholder de Kong (lib/cypher/encrypt.ts)
|
||||
Logging: server-logger (servidor) + client-logger (cliente, enruta a /api/logs)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Arquitectura obligatoria (Frontend Seguro)
|
||||
|
||||
```
|
||||
Server Components (Server-First):
|
||||
- Componentes por defecto.
|
||||
- Ejecutan lógica de negocio pesada, lectura de cookies y llamadas al backend.
|
||||
- Protegidos con 'serverRequireAuth' al inicio.
|
||||
|
||||
Client Components:
|
||||
- Exclusivos para interactividad (onClick, onSubmit, Zustand hooks).
|
||||
- Deben usar la directiva 'use client' explícitamente.
|
||||
- Mantenerse lo más atómicos y puros posible (sin lógica de negocio).
|
||||
|
||||
Route Handlers:
|
||||
- Ubicados en app/api/ y usados como proxies seguros para backend downstream o logs.
|
||||
|
||||
Estructura de Carpetas:
|
||||
app/ ← Páginas, layouts y API Route Handlers
|
||||
components/ui/ ← Componentes visuales puros y reutilizables
|
||||
lib/ ← Módulos utilitarios core (auth, logger, cypher)
|
||||
logs/ ← Archivo logs centralizado (logs/app.log)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Antes de implementar cualquier Pantalla o Componente
|
||||
|
||||
1. Completar `standards/05-client-insumos.md` con las especificaciones de pantalla/mockups.
|
||||
2. Confirmar los endpoints backend downstream y las políticas de logs.
|
||||
3. Asegurar que las variables de entorno estén registradas en `standards/04-configmap-management.md`.
|
||||
4. **Consultar Engram** antes de programar: `¿hay decisiones previas sobre este componente o integración?`
|
||||
|
||||
---
|
||||
|
||||
## Memoria persistente — Engram
|
||||
|
||||
Engram preserva las decisiones de diseño frontend que el agente suele olvidar al cerrar la sesión.
|
||||
|
||||
### Reglas de uso
|
||||
|
||||
```
|
||||
CUÁNDO LEER (siempre al inicio):
|
||||
- Antes de implementar una pantalla o componente → buscar decisiones previas del flujo
|
||||
- Antes de configurar variables o endpoints → buscar llaves configuradas anteriormente
|
||||
- Si un flujo de autenticación o encriptación falla → buscar gotchas documentados
|
||||
|
||||
CUÁNDO ESCRIBIR (al finalizar):
|
||||
- Cada decisión que altere la arquitectura base (ej. "usar mock de datos temporal para la cuenta de ahorros")
|
||||
- Cada llave de configuración agregada a .env
|
||||
- Cada gotcha o comportamiento no documentado de daisyUI o Next.js
|
||||
- El resultado del code-review-excellence si tuvo violaciones corregidas
|
||||
|
||||
FORMATO DE ESCRITURA:
|
||||
Componente/Flujo: <nombre del flujo o componente>
|
||||
Decisión: <qué se decidió y por qué>
|
||||
Fecha: <YYYY-MM-DD>
|
||||
Contexto: <breve explicación técnica>
|
||||
```
|
||||
|
||||
### Engram Cloud — Sincronización entre desarrolladores
|
||||
|
||||
```bash
|
||||
# Configurar el servidor cloud
|
||||
engram cloud config --server https://engram.pranical.com
|
||||
|
||||
# Enrollar el proyecto frontend-ai
|
||||
engram cloud enroll frontend-ai
|
||||
|
||||
# Sincronizar memorias del proyecto al cloud
|
||||
engram sync --cloud --project frontend-ai
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Orquestación Multi-Agente — Nivel 1 (activa)
|
||||
|
||||
El agente principal coordina y delega tareas a subagentes autónomos cuando se trabaja en componentes paralelos.
|
||||
|
||||
### Cuándo usar subagentes
|
||||
|
||||
```
|
||||
USAR subagentes cuando:
|
||||
- Se implementa el Layout general y la Página individual en paralelo
|
||||
- Se desarrolla una API Route (/api/...) y la UI del Cliente al mismo tiempo
|
||||
- Se genera documentación técnica de los componentes en paralelo
|
||||
|
||||
NO usar subagentes cuando:
|
||||
- Es debugging de estado compartido en Zustand
|
||||
- Se realizan cambios de middleware que impactan a toda la app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Convenciones de Log (Trazabilidad Centralizada)
|
||||
|
||||
```
|
||||
Server Logs:
|
||||
- Escritos directamente en logs/app.log a través de lib/logger/server-logger.ts.
|
||||
Client Logs:
|
||||
- Llaman a lib/logger/client-logger.ts, que envía un POST /api/logs para persistirlo en logs/app.log.
|
||||
|
||||
Formatos de Prefijos obligatorios:
|
||||
[INFO] Login success | {"userId":"123"}
|
||||
[WARN] Unauthorized access attempt | {"path":"/dashboard"}
|
||||
[ERROR] Backend connection failed | {"error":"ECONNREFUSED"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Control de Cookies y Headers Bancarios
|
||||
|
||||
```
|
||||
access_token ← Cookie HTTP-only, Secure (en prod), SameSite=strict
|
||||
refresh_token ← Cookie HTTP-only, Secure (en prod), SameSite=strict
|
||||
traceId ← deviceSessionReference del cliente propagado en las llamadas al API Route
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist pre-entrega (ejecutar siempre)
|
||||
|
||||
```
|
||||
Server Pages / Layouts:
|
||||
[ ] serverRequireAuth() invocado en la cabecera si la ruta es protegida
|
||||
[ ] Sin interactividad del cliente (sin onClick, useState, useAuthStore en este nivel)
|
||||
[ ] Manejo de errores perimetral con Next.js error.tsx
|
||||
|
||||
Client Components:
|
||||
[ ] 'use client' presente en la línea 1
|
||||
[ ] Estado global obtenido a través de useAuthStore o Zustand hooks específicos
|
||||
[ ] Consumo de datos sensibles encriptado/desencriptado usando lib/cypher
|
||||
[ ] Enrutado de logs a través de client-logger
|
||||
|
||||
Route Handlers (API):
|
||||
[ ] POST /api/logs recibe logs del cliente sin colisiones
|
||||
[ ] Autenticación de sesión validada antes de proxyar peticiones al backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de oro (nunca romper)
|
||||
|
||||
**Código:**
|
||||
1. **NUNCA** guardes JWT, secretos ni credenciales en `localStorage`, `sessionStorage` o variables de Javascript del cliente. Usa cookies HTTP-only.
|
||||
2. **NUNCA** realices llamadas directas al backend desde Client Components sin pasar por el middleware o API Route correspondiente si requiere auth.
|
||||
3. **NUNCA** utilices `useEffect` para sincronizar estados que pueden resolverse mediante React Server Components o Zustand.
|
||||
4. **SIEMPRE** registra logs de actividades críticas (login exitoso, fallido, logout, errores) en `logs/app.log`.
|
||||
5. **SIEMPRE** actualiza el grafo de Graphify (`graphify update .`) tras cambios estructurales.
|
||||
|
||||
**Memoria (Engram):**
|
||||
6. **SIEMPRE** consulta Engram antes de implementar para verificar decisiones previas del flujo.
|
||||
7. **SIEMPRE** registra en Engram las decisiones críticas y de diseño.
|
||||
8. **NUNCA** guardes secretos ni código completo en Engram.
|
||||
|
||||
---
|
||||
|
||||
## Checklist pre-PR (ejecutar desde el agente)
|
||||
|
||||
```
|
||||
Haz una revisión completa con code-review-excellence
|
||||
comparando la implementación contra el checklist de AGENT.md.
|
||||
Reporta cualquier violación antes de continuar.
|
||||
```
|
||||
178
INICIO-AQUI.md
Normal file
178
INICIO-AQUI.md
Normal file
@ -0,0 +1,178 @@
|
||||
# INICIO-AQUI.md — Punto de entrada para nuevos desarrollos frontend
|
||||
## La guía única que consolida todo el stack cognitivo y técnico de frontend-ai
|
||||
|
||||
**Para:** cualquier desarrollador de frontend o agente autónomo que empiece a trabajar en este proyecto
|
||||
**Tiempo estimado de lectura:** 10 minutos
|
||||
**Tiempo estimado antes de escribir código:** completar el §PASO 1
|
||||
|
||||
---
|
||||
|
||||
## Mapa del proyecto
|
||||
|
||||
```
|
||||
ibanking-ai-api/
|
||||
│
|
||||
├── AGENT.md ← SE CARGA AUTOMÁTICAMENTE en cada sesión de IA
|
||||
│ (system prompt de frontend — NO EDITAR sin consenso)
|
||||
│
|
||||
├── INICIO-AQUI.md ← ESTE ARCHIVO — leerlo primero
|
||||
│
|
||||
├── docker-compose.yml ← NUEVO: Servidor WireMock local para APIs downstream
|
||||
├── setup.sh ← NUEVO: Script de inicialización para Linux/Mac
|
||||
├── setup.ps1 ← NUEVO: Script de inicialización para Windows
|
||||
├── .env.local.template ← NUEVO: Plantilla de variables de entorno locales
|
||||
│
|
||||
├── mocks/ ← NUEVO: Mappings JSON de WireMock para simulación local
|
||||
│ └── mappings/
|
||||
│ ├── auth-me.json
|
||||
│ ├── cuentas.json
|
||||
│ └── transferencias.json
|
||||
│
|
||||
├── standards/ ← Referencia operacional durante la implementación
|
||||
│ ├── README.md
|
||||
│ ├── 01-log-tracing.md ← Server & client logging, enrutador POST /api/logs
|
||||
│ ├── 02-security-tracing.md ← Middleware de sesión, cookies HTTP-only, cifrado Kong
|
||||
│ ├── 03-headers-contract.md ← Propagación de traceId (sessionReference) y cookies JWT
|
||||
│ ├── 04-configmap-management.md ← Gestión de .env.local, NEXT_PUBLIC_ y server private keys
|
||||
│ └── 05-client-insumos.md ← ⭐ COMPLETAR CON EL DISEÑADOR/CLIENTE antes de empezar
|
||||
│
|
||||
├── docs/ ← Guías de referencia y visión del proyecto
|
||||
│ ├── guia-4-implemented-pattern.md ← Los patrones clave de Next.js App Router y Zustand
|
||||
│ ├── guia-4.5-skills-proposal.md ← Skills instalables y directrices del agente
|
||||
│ ├── guia-prompting-v4.md ← Plantilla del prompt para generar UI robusta y segura
|
||||
│ └── guia-v5-advanced-ai-architecture.md ← Integración avanzada con Engram Cloud + Graphify
|
||||
│
|
||||
├── .agents/ ← Skills congeladas comunitarias para excelencia operativa
|
||||
│ ├── README.md
|
||||
│ └── skills/
|
||||
│ ├── test-driven-development/SKILL.md
|
||||
│ ├── systematic-debugging/SKILL.md
|
||||
│ ├── code-review-excellence/SKILL.md
|
||||
│ ├── documentation-writer/SKILL.md
|
||||
│ └── security-best-practices/SKILL.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 1 — Antes de abrir el editor (obligatorio)
|
||||
|
||||
### 1.1 Ejecutar el script de Setup Automatizado (Onboarding)
|
||||
Ejecuta el script correspondiente a tu sistema operativo en la raíz del proyecto para inicializar el entorno, instalar dependencias de IA (Graphify, Engram) y crear tu archivo de configuración `.env.local` automáticamente:
|
||||
|
||||
```bash
|
||||
# Para desarrolladores en macOS / Linux:
|
||||
chmod +x setup.sh && ./setup.sh
|
||||
|
||||
# Para desarrolladores en Windows (PowerShell):
|
||||
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process; .\setup.ps1
|
||||
```
|
||||
|
||||
### 1.2 Levantar el Entorno de Mocks (Docker)
|
||||
Para interactuar localmente con datos simulados y no quedar bloqueado por caídas de red o fallos en el backend:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
Esto levantará el servidor **WireMock** en `http://localhost:8080` simulando de forma automática las respuestas del backend (Auth, Cuentas, y Transferencias).
|
||||
|
||||
### 1.3 Completar los insumos de pantalla y flujos
|
||||
Abrir `standards/05-client-insumos.md` y completar las secciones de la interfaz de usuario con el cliente o equipo de producto:
|
||||
|
||||
* §1 Contrato de APIs e Integraciones (endpoints del backend Spring Boot)
|
||||
* §2 Estructura y Comportamiento de la UI (Mockups, layouts y estados visuales)
|
||||
* §3 Gestión de Estado en Cliente (variables a registrar en Zustand)
|
||||
* §4 Niveles de Autenticación y Seguridad (páginas protegidas por middleware)
|
||||
* §5 Trazabilidad y Eventos Críticos (qué logs registrar)
|
||||
|
||||
### 1.4 Verificar presencia de Skills locales
|
||||
Los skills comunitarios están congelados y listos en `.agents/skills/`:
|
||||
* `test-driven-development/SKILL.md` (tdd estricto en frontend con Vitest/Testing Library)
|
||||
* `systematic-debugging/SKILL.md` (diagnóstico paso a paso de estado e hidratación React)
|
||||
* `code-review-excellence/SKILL.md` (checklist de accesibilidad, seguridad y rendimiento)
|
||||
* `documentation-writer/SKILL.md` (documentación de Props y flujo de Zustand)
|
||||
* `security-best-practices/SKILL.md` (sanitización, cookies y seguridad Kong en client)
|
||||
|
||||
### 1.5 Generar y actualizar el Knowledge Graph con Graphify
|
||||
Para optimizar las búsquedas de código y componentes del agente:
|
||||
```bash
|
||||
# Registrar mcpServer local de Graphify (según se detalla en README.md)
|
||||
# Actualizar el grafo tras cambios estructurales:
|
||||
graphify update .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 2 — El prompt de implementación de componentes
|
||||
|
||||
Plantilla de prompt rápido para inyectar al agente:
|
||||
```
|
||||
Implementa una pantalla/componente en Next.js siguiendo AGENT.md y docs/guia-4-implemented-pattern.md.
|
||||
|
||||
Reglas del Frontend Seguro:
|
||||
- NO uses cookies ni secrets en el código del cliente.
|
||||
- Utiliza Server Components por defecto. Usa 'use client' solo si hay interactividad.
|
||||
- Centraliza estados globales en el store de Zustand en lib/auth/client/useAuthStore.ts.
|
||||
- Registra logs críticos llamando al logger del cliente (client-logger) o servidor.
|
||||
|
||||
### Contexto de la Pantalla:
|
||||
- Nombre: [ej. Dashboard / Perfil / Historial]
|
||||
- Ruta: [ej. /dashboard o /api/user]
|
||||
- Componentes UI requeridos: [ej. daisyUI tables, custom buttons]
|
||||
|
||||
### Insumos de la UI:
|
||||
[pegar especificación de standards/05-client-insumos.md]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 3 — Control de versiones y convencionalismo de commits
|
||||
|
||||
```bash
|
||||
# Crear rama
|
||||
git checkout -b feat/pantalla-<nombre-flujo>-agented
|
||||
|
||||
# Commit estructurado (Conventional Commit sin atribución AI)
|
||||
git commit -m "feat(ui): [FRONTEND-SECURE] implementar pantalla de <nombre-flujo>
|
||||
|
||||
Pattern: Server-First, Zustand State
|
||||
Skills: security-best-practices, code-review-excellence
|
||||
Standards: standards/05-client-insumos.md completado"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 4 — Checklist pre-PR (ejecutar desde el agente)
|
||||
|
||||
```
|
||||
Haz una revisión completa con code-review-excellence
|
||||
comparando la implementación contra el checklist de AGENT.md.
|
||||
Reporta cualquier violación antes de continuar.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Referencia rápida — Los 5 pilares del Frontend Banesco
|
||||
|
||||
| # | Pilar | Solución Clave |
|
||||
|---|---|---|
|
||||
| 1 | **Server-First** | Server Components por defecto para fetching seguro y veloz |
|
||||
| 2 | **Cookies Protegidas** | Cookies HTTP-only administradas server-side para JWT |
|
||||
| 3 | **Gobernanza de Estado** | Stores Zustand puros y reactivos sin excesivos Contexts |
|
||||
| 4 | **Logs Enrutados** | Centralización de logs del cliente mediante `/api/logs` a `logs/app.log` |
|
||||
| 5 | **Cifrado Perimetral** | Encriptación Kong en datos confidenciales que viajan al cliente |
|
||||
|
||||
---
|
||||
|
||||
## ¿Cuándo consultar cada guía?
|
||||
|
||||
| Situación | Dónde ir |
|
||||
|---|---|
|
||||
| Primera vez en el proyecto | **Este archivo** |
|
||||
| Definir campos de UI e integraciones | `standards/05-client-insumos.md` |
|
||||
| Implementar logs en el cliente o servidor | `standards/01-log-tracing.md` |
|
||||
| Autenticar rutas o encriptar datos | `standards/02-security-tracing.md` |
|
||||
| Revisar el flujo de cookies y traceId | `standards/03-headers-contract.md` |
|
||||
| Configurar archivos .env o variables públicas | `standards/04-configmap-management.md` |
|
||||
| Entender patrones de Server/Client Components | `docs/guia-4-implemented-pattern.md` |
|
||||
| Ver la propuesta de Skills y tooling | `docs/guia-4.5-skills-proposal.md` |
|
||||
| Plantilla de prompt interactivo de UI | `docs/guia-prompting-v4.md` |
|
||||
| Activar Engram Cloud y Graphify | `docs/guia-v5-advanced-ai-architecture.md` |
|
||||
358
app/api/auth/login/route.ts
Normal file
358
app/api/auth/login/route.ts
Normal file
@ -0,0 +1,358 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { apiClient } from '@/lib/utils/api-client';
|
||||
import { logInfo, logWarn, logError } from '@/lib/logger/server-logger';
|
||||
|
||||
const LOGIN_ENDPOINT = '/auth/login';
|
||||
const ACCESS_TOKEN_COOKIE = 'access_token';
|
||||
const REFRESH_TOKEN_COOKIE = 'refresh_token';
|
||||
const MAX_PASSWORD_ATTEMPTS = 3;
|
||||
const MAX_TOKEN_ATTEMPTS = 3;
|
||||
|
||||
// Contadores de intentos en memoria y mapa de sesiones activas
|
||||
const userAttempts = new Map<string, { passwordAttempts: number; tokenAttempts: number; lastAttempt: Date }>();
|
||||
const activeSessions = new Map<string, { sessionId: string; lastActivity: Date }>();
|
||||
|
||||
interface LoginError {
|
||||
code: string;
|
||||
message: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
function validateUsername(username: string): LoginError | null {
|
||||
if (!username || username.trim() === '') {
|
||||
return {
|
||||
code: 'USERNAME_REQUIRED',
|
||||
message: 'Este campo es obligatorio',
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
if (username.length < 6) {
|
||||
return {
|
||||
code: 'USERNAME_TOO_SHORT',
|
||||
message: 'Debe ser mayor o igual a seis caracteres',
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
if (username.length > 12) {
|
||||
return {
|
||||
code: 'USERNAME_TOO_LONG',
|
||||
message: 'El sistema solo debe permitir ingresar 12 caracteres',
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validatePassword(password: string): LoginError | null {
|
||||
if (!password || password.trim() === '') {
|
||||
return {
|
||||
code: 'PASSWORD_REQUIRED',
|
||||
message: 'Este campo es obligatorio',
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getUserAttempts(username: string) {
|
||||
let attempts = userAttempts.get(username);
|
||||
if (!attempts) {
|
||||
attempts = { passwordAttempts: 0, tokenAttempts: 0, lastAttempt: new Date() };
|
||||
userAttempts.set(username, attempts);
|
||||
}
|
||||
return attempts;
|
||||
}
|
||||
|
||||
function resetPasswordAttempts(username: string) {
|
||||
const attempts = getUserAttempts(username);
|
||||
attempts.passwordAttempts = 0;
|
||||
attempts.lastAttempt = new Date();
|
||||
}
|
||||
|
||||
function incrementPasswordAttempts(username: string): number {
|
||||
const attempts = getUserAttempts(username);
|
||||
attempts.passwordAttempts += 1;
|
||||
attempts.lastAttempt = new Date();
|
||||
return attempts.passwordAttempts;
|
||||
}
|
||||
|
||||
function incrementTokenAttempts(username: string): number {
|
||||
const attempts = getUserAttempts(username);
|
||||
attempts.tokenAttempts += 1;
|
||||
attempts.lastAttempt = new Date();
|
||||
return attempts.tokenAttempts;
|
||||
}
|
||||
|
||||
function resetTokenAttempts(username: string) {
|
||||
const attempts = getUserAttempts(username);
|
||||
attempts.tokenAttempts = 0;
|
||||
attempts.lastAttempt = new Date();
|
||||
}
|
||||
|
||||
function checkActiveSession(username: string, sessionId?: string): boolean {
|
||||
const session = activeSessions.get(username);
|
||||
if (session && session.sessionId !== sessionId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setActiveSession(username: string, sessionId: string) {
|
||||
activeSessions.set(username, { sessionId, lastActivity: new Date() });
|
||||
}
|
||||
|
||||
function getErrorMessage(error: LoginError, userStatus?: string, userType?: string): string {
|
||||
const { code, message } = error;
|
||||
|
||||
if (['USERNAME_REQUIRED', 'PASSWORD_REQUIRED', 'TOKEN_REQUIRED'].includes(code)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const staticMessages: Record<string, string> = {
|
||||
USERNAME_TOO_SHORT: 'Debe ser mayor o igual a seis caracteres',
|
||||
USERNAME_TOO_LONG: 'El sistema solo debe permitir ingresar 12 caracteres',
|
||||
USER_NOT_FOUND: 'Usuario o Clave Inválida',
|
||||
USER_SUSPENDED: 'Tu usuario ha sido suspendido. Ingresa en la opción Autogestión de Usuario',
|
||||
USER_INACTIVE: 'Usuario inactivo. Por favor comunícate con Soporte Empresas a través del (0212-501.5500)',
|
||||
ACTIVE_SESSION: 'Usuario activo en otra sesión. Por tu seguridad cierra la sesión y vuelve a ingresar tu usuario',
|
||||
COMPANY_INACTIVE: 'Empresa inactiva. Por favor comunícate con Soporte',
|
||||
};
|
||||
|
||||
if (staticMessages[code]) {
|
||||
return staticMessages[code];
|
||||
}
|
||||
|
||||
if (code === 'USER_BLOCKED') {
|
||||
return userType === 'master'
|
||||
? 'Usuario bloqueado. Por favor comunícate con Soporte Exterior Empresas a través del (0212-501.5500)'
|
||||
: 'Tu usuario se encuentra bloqueado. Ingresa en la opción Autogestión de Usuario';
|
||||
}
|
||||
|
||||
if (code === 'INVALID_PASSWORD') {
|
||||
return getInvalidPasswordMessage(message);
|
||||
}
|
||||
|
||||
if (code === 'INVALID_TOKEN') {
|
||||
return getInvalidTokenMessage(message);
|
||||
}
|
||||
|
||||
return message || 'Error al iniciar sesión';
|
||||
}
|
||||
|
||||
function getInvalidPasswordMessage(message: string): string {
|
||||
const username = message.split('|')[1] ?? '';
|
||||
if (username) {
|
||||
const attempts = getUserAttempts(username);
|
||||
if (attempts.passwordAttempts >= MAX_PASSWORD_ATTEMPTS) {
|
||||
return 'Tu usuario ha sido suspendido. Ingresa a la opción Autogestión de Usuario';
|
||||
}
|
||||
}
|
||||
return 'Usuario o Clave Inválida';
|
||||
}
|
||||
|
||||
function getInvalidTokenMessage(message: string): string {
|
||||
const username = message.split('|')[1] ?? '';
|
||||
if (username) {
|
||||
const { tokenAttempts } = getUserAttempts(username);
|
||||
switch (tokenAttempts) {
|
||||
case 1:
|
||||
return 'Código inválido';
|
||||
case 2:
|
||||
return 'Código inválido, al tercer intento tu usuario será bloqueado';
|
||||
default:
|
||||
if (tokenAttempts >= MAX_TOKEN_ATTEMPTS) {
|
||||
return 'Código inválido, tu usuario ha sido bloqueado. Por favor ingresa a la opción Autogestión de Usuario';
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'Código inválido';
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { userName, password, softToken } = body;
|
||||
|
||||
// 1. Validaciones perimetrales
|
||||
const usernameError = validateUsername(userName);
|
||||
if (usernameError) {
|
||||
return NextResponse.json({ error: getErrorMessage(usernameError) }, { status: usernameError.status });
|
||||
}
|
||||
|
||||
const passwordError = validatePassword(password);
|
||||
if (passwordError) {
|
||||
return NextResponse.json({ error: getErrorMessage(passwordError) }, { status: passwordError.status });
|
||||
}
|
||||
|
||||
// 2. Control de sesión activa simultánea
|
||||
const existingSessionId = request.headers.get('x-session-id');
|
||||
const sessionId = existingSessionId || `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
if (checkActiveSession(userName, sessionId)) {
|
||||
logWarn('Intento de doble sesión denegado', { userName });
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage({ code: 'ACTIVE_SESSION', message: '', status: 403 }) },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const loginPayload: any = { userName, password };
|
||||
if (softToken) {
|
||||
loginPayload.softToken = softToken;
|
||||
}
|
||||
|
||||
// 3. Consumir backend Spring Boot mockeado
|
||||
const response = await apiClient.post<any>(
|
||||
LOGIN_ENDPOINT,
|
||||
loginPayload,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeoutMs: 10000,
|
||||
}
|
||||
);
|
||||
|
||||
// 4. Retornar desafío 2FA si corresponde
|
||||
if (response.requires2FA && !softToken) {
|
||||
logInfo('Segundo factor (2FA) requerido para el usuario', { userName });
|
||||
return NextResponse.json({
|
||||
requires2FA: true,
|
||||
message: 'Ingresa el código de autenticación',
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Validaciones de estado de cuenta
|
||||
const userStatus = response.user?.status || response.accountStatus;
|
||||
const userType = response.user?.userType;
|
||||
|
||||
if (userStatus === 'bloqueado') {
|
||||
const errorMsg = getErrorMessage({ code: 'USER_BLOCKED', message: '', status: 403 }, userStatus, userType);
|
||||
return NextResponse.json({ error: errorMsg }, { status: 403 });
|
||||
}
|
||||
|
||||
if (userStatus === 'suspendido') {
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage({ code: 'USER_SUSPENDED', message: '', status: 403 }) },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
if (userStatus === 'inactivo') {
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage({ code: 'USER_INACTIVE', message: '', status: 403 }) },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
if (userStatus === 'reiniciado') {
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage({ code: 'USER_RESET', message: '', status: 403 }) },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
if (userStatus !== 'activo') {
|
||||
return NextResponse.json({ error: 'Usuario no activo' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 6. Login exitoso - Resetear contadores de intentos
|
||||
resetPasswordAttempts(userName);
|
||||
resetTokenAttempts(userName);
|
||||
setActiveSession(userName, sessionId);
|
||||
|
||||
const nextResponse = NextResponse.json({
|
||||
success: true,
|
||||
user: response.user,
|
||||
requires2FA: false,
|
||||
message: 'Login exitoso',
|
||||
});
|
||||
|
||||
// 7. Establecer cookies JWT HTTP-only
|
||||
if (response.token) {
|
||||
const maxAge = response.expiresIn || 3600;
|
||||
nextResponse.cookies.set(ACCESS_TOKEN_COOKIE, response.token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: maxAge,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
if (response.refreshToken) {
|
||||
nextResponse.cookies.set(REFRESH_TOKEN_COOKIE, response.refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
logInfo('Login exitoso con 2FA verificado', {
|
||||
userId: response.user?.id,
|
||||
userName: response.user?.userName,
|
||||
userType: response.user?.userType,
|
||||
});
|
||||
|
||||
return nextResponse;
|
||||
|
||||
} catch (backendError: any) {
|
||||
const errorStatus = backendError.response?.status || 500;
|
||||
const errorMessage = backendError.message || '';
|
||||
|
||||
// 8. Control de intentos fallidos
|
||||
if (errorStatus === 401 || errorMessage.includes('Invalid') || errorMessage.includes('not found')) {
|
||||
if (softToken) {
|
||||
const tokenAttempts = incrementTokenAttempts(userName);
|
||||
logWarn('Código 2FA ingresado inválido', { userName, tokenAttempts });
|
||||
|
||||
if (tokenAttempts >= MAX_TOKEN_ATTEMPTS) {
|
||||
logError('Usuario bloqueado por superar intentos de token 2FA', { userName });
|
||||
const errorMsg = getErrorMessage({
|
||||
code: 'INVALID_TOKEN',
|
||||
message: `|${userName}`,
|
||||
status: 403,
|
||||
});
|
||||
return NextResponse.json({ error: errorMsg }, { status: 403 });
|
||||
}
|
||||
|
||||
const errorMsg = getErrorMessage({
|
||||
code: 'INVALID_TOKEN',
|
||||
message: `|${userName}`,
|
||||
status: 400,
|
||||
});
|
||||
return NextResponse.json({ error: errorMsg }, { status: 400 });
|
||||
} else {
|
||||
const passwordAttempts = incrementPasswordAttempts(userName);
|
||||
logWarn('Credenciales de contraseña inválidas', { userName, passwordAttempts });
|
||||
|
||||
if (passwordAttempts >= MAX_PASSWORD_ATTEMPTS) {
|
||||
logError('Usuario suspendido por superar intentos de contraseña', { userName });
|
||||
const errorMsg = getErrorMessage({
|
||||
code: 'INVALID_PASSWORD',
|
||||
message: `|${userName}`,
|
||||
status: 403,
|
||||
});
|
||||
return NextResponse.json({ error: errorMsg }, { status: 403 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage({ code: 'INVALID_PASSWORD', message: `|${userName}`, status: 401 }) },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Error al iniciar sesión. Por favor, intenta de nuevo.' },
|
||||
{ status: errorStatus }
|
||||
);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
logError('Fallo en el servidor de Login', { error: error.message });
|
||||
return NextResponse.json({ error: 'Error al procesar la solicitud de login' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
8
app/globals.css
Normal file
8
app/globals.css
Normal file
@ -0,0 +1,8 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--primary: #00875A;
|
||||
/* Color Verde Corporativo de BD */
|
||||
}
|
||||
21
app/layout.tsx
Normal file
21
app/layout.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'BD - Internet Banking',
|
||||
description: 'Portal Seguro de Internet Banking',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="es">
|
||||
<body>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
324
app/login/LoginForm.tsx
Normal file
324
app/login/LoginForm.tsx
Normal file
@ -0,0 +1,324 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthStore } from '@/lib/auth/client/useAuthStore';
|
||||
import { apiClient } from '@/lib/utils/api-client';
|
||||
import { logInfo, logError } from '@/lib/logger/client-logger';
|
||||
|
||||
type LoginStep = 'credentials' | 'twoFactor';
|
||||
|
||||
export default function LoginForm() {
|
||||
const [step, setStep] = useState<LoginStep>('credentials');
|
||||
const [userName, setUserName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [softToken, setSoftToken] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [userNameError, setUserNameError] = useState('');
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
const [tokenError, setTokenError] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showDeactivateToken, setShowDeactivateToken] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { setUser, setRequires2FA, setLoading } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (userName.length > 12) {
|
||||
setUserName(userName.substring(0, 12));
|
||||
}
|
||||
}, [userName]);
|
||||
|
||||
const validateUserName = (value: string): string => {
|
||||
if (!value || value.trim() === '') {
|
||||
return 'Este campo es obligatorio';
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return 'Debe ser mayor o igual a seis caracteres';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const validatePassword = (value: string): string =>
|
||||
value?.trim() ? '' : 'Este campo es obligatorio';
|
||||
|
||||
const validateToken = (value: string): string =>
|
||||
value?.trim() ? '' : 'Este campo es obligatorio';
|
||||
|
||||
const handleUserNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setUserName(value);
|
||||
setUserNameError(validateUserName(value));
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setPassword(value);
|
||||
setPasswordError(validatePassword(value));
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setSoftToken(value);
|
||||
setTokenError(validateToken(value));
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleCredentialsSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
const userNameErr = validateUserName(userName);
|
||||
const passwordErr = validatePassword(password);
|
||||
|
||||
setUserNameError(userNameErr);
|
||||
setPasswordError(passwordErr);
|
||||
|
||||
if (userNameErr || passwordErr) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await apiClient.post<any>('/api/auth/login', { userName, password });
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
if (response.requires2FA) {
|
||||
setRequires2FA(true, userName);
|
||||
setStep('twoFactor');
|
||||
setShowDeactivateToken(true);
|
||||
} else {
|
||||
setUser(response.user);
|
||||
logInfo('Login exitoso de credenciales', { userId: response.user.id });
|
||||
router.push('/dashboard');
|
||||
router.refresh();
|
||||
}
|
||||
} catch (err: any) {
|
||||
logError('Error en credenciales de login', { error: err.message });
|
||||
setError(err.message || 'Error desconocido.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTwoFactorSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
const tokenErr = validateToken(softToken);
|
||||
setTokenError(tokenErr);
|
||||
|
||||
if (tokenErr) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await apiClient.post<any>('/api/auth/login', { userName, password, softToken });
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
setUser(response.user);
|
||||
logInfo('Login exitoso con 2FA completado', { userId: response.user.id });
|
||||
router.push('/dashboard');
|
||||
router.refresh();
|
||||
} catch (err: any) {
|
||||
logError('Fallo de validacion de token 2FA', { error: err.message });
|
||||
setError(err.message || 'Código de autenticación inválido.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isCredentialsButtonDisabled = !userName || !password || isLoading || !!userNameError || !!passwordError;
|
||||
const isTokenButtonDisabled = !softToken || isLoading || !!tokenError;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={step === 'credentials' ? handleCredentialsSubmit : handleTwoFactorSubmit}
|
||||
className="space-y-6"
|
||||
>
|
||||
{error && (
|
||||
<div className="bg-error/10 border border-error rounded-lg p-4 flex items-start gap-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5 text-error flex-shrink-0 mt-0.5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-error text-sm font-medium">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'credentials' ? (
|
||||
<>
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Usuario</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ingresa tu usuario"
|
||||
value={userName}
|
||||
onChange={handleUserNameChange}
|
||||
required
|
||||
maxLength={12}
|
||||
className={`input input-bordered w-full pl-10 ${
|
||||
userNameError ? 'input-error' : ''
|
||||
}`}
|
||||
/>
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
{userNameError && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">{userNameError}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Clave</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Ingresa tu contraseña"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
className={`input input-bordered w-full pl-10 pr-10 ${
|
||||
passwordError ? 'input-error' : ''
|
||||
}`}
|
||||
/>
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/50 hover:text-base-content"
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l18 18" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{passwordError && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">{passwordError}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full text-white"
|
||||
disabled={isCredentialsButtonDisabled}
|
||||
>
|
||||
{isLoading ? 'Ingresando...' : 'Ingresar'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center space-y-2">
|
||||
<h3 className="text-lg font-bold">Autenticación de dos factores (2FA)</h3>
|
||||
<p className="text-sm text-base-content/70">
|
||||
Ingresa el código numérico de 6 dígitos de tu aplicación móvil Soft Token.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold">Código OTP</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="000000"
|
||||
value={softToken}
|
||||
onChange={handleTokenChange}
|
||||
required
|
||||
maxLength={6}
|
||||
className={`input input-bordered w-full text-center tracking-widest text-lg font-mono ${
|
||||
tokenError ? 'input-error' : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
{tokenError && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">{tokenError}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDeactivateToken && (
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-base-content/60">
|
||||
¿No tienes acceso a tu móvil? Ingresa en Autogestión.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full text-white"
|
||||
disabled={isTokenButtonDisabled}
|
||||
>
|
||||
{isLoading ? 'Verificando...' : 'Verificar código'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline w-full"
|
||||
onClick={() => {
|
||||
setStep('credentials');
|
||||
setSoftToken('');
|
||||
setTokenError('');
|
||||
setError('');
|
||||
}}
|
||||
>
|
||||
Volver
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
21
app/login/page.tsx
Normal file
21
app/login/page.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import LoginForm from './LoginForm';
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-base-200 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="text-center">
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-primary">
|
||||
BD
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-base-content/60">
|
||||
Portal de Internet Banking Seguro
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-base-100 py-8 px-4 shadow sm:rounded-lg sm:px-10 border border-base-300">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
app/page.tsx
Normal file
6
app/page.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function RootPage() {
|
||||
// Redirección segura al flujo de login
|
||||
redirect('/login');
|
||||
}
|
||||
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@ -0,0 +1,16 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Servidor de Mocks de Backend (WireMock)
|
||||
# Simula el comportamiento del backend de Spring Boot (Java) y el API de seguridad de Banesco.
|
||||
backend-mock:
|
||||
image: wiremock/wiremock:3.2.0-alpine
|
||||
container_name: ibanking-backend-mock
|
||||
ports:
|
||||
- "8080:8080" # Corre en el puerto estándar del backend
|
||||
volumes:
|
||||
- ./mocks:/home/wiremock
|
||||
command:
|
||||
- --verbose
|
||||
- --global-response-templating
|
||||
restart: unless-stopped
|
||||
129
docs/guia-4-implemented-pattern.md
Normal file
129
docs/guia-4-implemented-pattern.md
Normal file
@ -0,0 +1,129 @@
|
||||
# Guía 4 — Patrones Arquitectónicos Aprobados
|
||||
## Catálogo de patrones de desarrollo seguro y limpio para Next.js App Router
|
||||
|
||||
> **Propósito:** Esta guía documenta los patrones de código oficiales del proyecto. Cualquier desvío de estos patrones debe estar debidamente motivado y registrado en Engram.
|
||||
|
||||
---
|
||||
|
||||
## 1. El Patrón Server-First (Server Components por defecto)
|
||||
|
||||
En Next.js App Router, todos los componentes de la carpeta `app/` son **React Server Components** por defecto. Esto significa que se renderizan en el servidor, reduciendo el tamaño del bundle de JavaScript en el cliente y protegiendo accesos a base de datos o APIs downstream.
|
||||
|
||||
### Reglas del Patrón:
|
||||
1. **Fetching Directo:** Las peticiones al backend Spring Boot se realizan directamente en la función asíncrona del componente, sin necesidad de usar `useEffect` o estados de React en el cliente.
|
||||
2. **Seguridad Integrada:** Se verifica la sesión del usuario al inicio del renderizado usando `serverRequireAuth()`.
|
||||
|
||||
```typescript
|
||||
// app/dashboard/page.tsx
|
||||
import { serverRequireAuth } from '@/lib/auth/middleware';
|
||||
import { fetchCuentasUsuario } from '@/standards/03-headers-contract';
|
||||
|
||||
export default async function DashboardPage() {
|
||||
// 1. Validar sesión en servidor
|
||||
const user = await serverRequireAuth();
|
||||
|
||||
// 2. Fetching asíncrono seguro
|
||||
const cuentas = await fetchCuentasUsuario(user.sessionReference);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Portal de Cuentas de {user.name}</h1>
|
||||
<ul className="space-y-4">
|
||||
{cuentas.map((cta) => (
|
||||
<li key={cta.id} className="p-4 bg-base-100 rounded-box shadow">
|
||||
Cuenta: {cta.numero} - Saldo: ${cta.saldo}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. El Patrón de Hidratación de Componentes del Cliente
|
||||
|
||||
Cuando requerimos interactividad del usuario (clicks, formularios reactivos, modales dinámicos), declaramos un **Client Component** usando la directiva `'use client'` al principio del archivo.
|
||||
|
||||
### Reglas de Diseño:
|
||||
- **Área Limpia:** Mantén el Client Component lo más atómico y libre de fetching posible. Pasa los datos iniciales obtenidos en el Server Component como *Props*.
|
||||
- **Estado Global:** Si el componente altera el estado de la sesión, actualiza el store de Zustand.
|
||||
|
||||
```typescript
|
||||
// components/BotonTransferencia.tsx
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { logInfo } from '@/lib/logger/client-logger';
|
||||
import { useAuthStore } from '@/lib/auth/client/useAuthStore';
|
||||
|
||||
interface BotonProps {
|
||||
cuentaOrigenId: string;
|
||||
}
|
||||
|
||||
export default function BotonTransferencia({ cuentaOrigenId }: BotonProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { user } = useAuthStore();
|
||||
|
||||
const handleTransfer = async () => {
|
||||
setLoading(true);
|
||||
logInfo('Acción de transferencia disparada', { cuentaOrigenId, userId: user?.id });
|
||||
|
||||
// Simular llamada de API local
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleTransfer}
|
||||
disabled={loading}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{loading ? 'Procesando...' : 'Transferir Saldo'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Gobernanza de Estado Cliente con Zustand
|
||||
|
||||
Evitamos el uso de React Context APIs redundantes o Redux pesado. El estado de la sesión cliente y configuraciones UI se centralizan en Zustand stores ubicados en `lib/auth/client/useAuthStore.ts`.
|
||||
|
||||
### Reglas del Patrón:
|
||||
1. **Reactividad Fina:** Usa selectores en tus componentes para suscribirte solo a las propiedades requeridas (evita re-renders innecesarios).
|
||||
2. **Acciones Claras:** Toda lógica que muta el estado debe encapsularse en las acciones del store.
|
||||
|
||||
```typescript
|
||||
// lib/auth/client/useAuthStore.ts
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
login: (user: User) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
login: (user) => set({ user, isAuthenticated: true }),
|
||||
logout: () => set({ user: null, isAuthenticated: false }),
|
||||
}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. El Patrón de Logging Proxy para el Cliente
|
||||
|
||||
Como el cliente no tiene acceso al sistema de archivos local (`logs/app.log`), canalizamos todas las trazas de Client Components mediante el proxy seguro de Next.js `/api/logs`.
|
||||
|
||||
* Ver `standards/01-log-tracing.md` para el flujo de llamadas y enrutamiento técnico de este patrón.
|
||||
50
docs/guia-4.5-skills-proposal.md
Normal file
50
docs/guia-4.5-skills-proposal.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Guía 4.5 — Habilidades de Agente y Comunidad (Skills)
|
||||
## El ecosistema de automatización cognitiva y gobernanza de código
|
||||
|
||||
> **Propósito:** Esta guía documenta cómo los agentes de IA resuelven y ejecutan tareas críticas basándose en los skills congelados localmente en el directorio `.agents/skills/`.
|
||||
|
||||
---
|
||||
|
||||
## 1. ¿Qué son los Skills Congelados?
|
||||
|
||||
Los **Skills** son directrices operacionales estructuradas en archivos `SKILL.md` que le indican al agente de IA exactamente cómo ejecutar ciertas disciplinas técnicas (como escribir pruebas, auditar seguridad o revisar código) bajo las reglas específicas de Banesco.
|
||||
|
||||
Al estar ubicadas localmente dentro del repositorio en `.agents/skills/`, garantizan que cualquier agente que trabaje en este proyecto las cargue automáticamente, previniendo la degradación y el olvido de convenciones.
|
||||
|
||||
---
|
||||
|
||||
## 2. Los 5 Skills Comunitarios Instalados
|
||||
|
||||
En el proyecto `frontend-ai` contamos con 5 skills core:
|
||||
|
||||
### 2.1 Test-Driven Development (`.agents/skills/test-driven-development/SKILL.md`)
|
||||
- **Objetivo:** Escribir pruebas unitarias robustas en frontend (Vitest / Testing Library) antes de dar por completado un componente.
|
||||
- **Enfoque:** Pruebas de renderizado, testing de interactividad (userEvent), mockeo de peticiones fetch y Zustand stores.
|
||||
|
||||
### 2.2 Systematic Debugging (`.agents/skills/systematic-debugging/SKILL.md`)
|
||||
- **Objetivo:** Resolver problemas de estado, hidratación ("hydration mismatch") y dependencias rotas de daisyUI.
|
||||
- **Enfoque:** Proceso analítico de 4 fases para aislar el error, reproducir localmente y corregir la causa raíz.
|
||||
|
||||
### 2.3 Code Review Excellence (`.agents/skills/code-review-excellence/SKILL.md`)
|
||||
- **Objetivo:** Auditar la calidad del código antes de enviar a PR.
|
||||
- **Enfoque:** Validaciones estrictas contra el checklist de `AGENT.md`, detección de fugas de secretos y estándares de rendimiento.
|
||||
|
||||
### 2.4 Documentation Writer (`.agents/skills/documentation-writer/SKILL.md`)
|
||||
- **Objetivo:** Mantener la documentación técnica auto-generada actualizada.
|
||||
- **Enfoque:** Documentación clara de Props TypeScript, estados globales en Zustand y esquemas de API Routes.
|
||||
|
||||
### 2.5 Security Best Practices (`.agents/skills/security-best-practices/SKILL.md`)
|
||||
- **Objetivo:** Asegurar el frontend contra vulnerabilidades bancarias comunes.
|
||||
- **Enfoque:** Sanitización de HTML, prevención de XSS, gestión de cookies con flags seguras y encriptación de datos sensibles.
|
||||
|
||||
---
|
||||
|
||||
## 3. Cómo Activar los Skills en tu Sesión de IA
|
||||
|
||||
Cuando interactúes con tu agente, puedes pedirle explícitamente que aplique un skill particular:
|
||||
|
||||
```
|
||||
"Agente, implementa el formulario de transferencia y luego activa el skill 'security-best-practices' para validar que no haya fugas de datos y que las cookies se gestionen según los estándares de seguridad."
|
||||
```
|
||||
|
||||
El agente leerá el archivo `.agents/skills/security-best-practices/SKILL.md` y aplicará las directrices precisas durante el desarrollo.
|
||||
69
docs/guia-prompting-v4.md
Normal file
69
docs/guia-prompting-v4.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Guía de Prompting V4 — Desarrollo con IA
|
||||
## Guía y plantillas de prompts de alta fidelidad para el desarrollo guiado por agentes autónomos
|
||||
|
||||
> **Propósito:** Esta guía proporciona a los desarrolladores la plantilla oficial para redactar prompts claros, estructurados y libres de ambigüedad al delegar la creación de UI a un agente de Inteligencia Artificial.
|
||||
|
||||
---
|
||||
|
||||
## 1. Filosofía de Prompting Seguro y de Alto Impacto
|
||||
|
||||
Para que la IA produzca interfaces que sigan el **100% de los estándares de seguridad y rendimiento**, los prompts deben proveer contexto explícito y limitar el alcance del desarrollo.
|
||||
|
||||
- **Evita prompts ambiguos:** *"Hazme una pantalla de dashboard"* genera código genérico que viola la arquitectura de Next.js y el cifrado de datos.
|
||||
- **Uso estructurado:** Divide tus solicitudes en: Contexto de Negocio, Enrutamiento, Especificaciones de UI, Integración de APIs, y Activación de Skills.
|
||||
|
||||
---
|
||||
|
||||
## 2. Plantilla Oficial de Prompt para Pantallas / Componentes
|
||||
|
||||
Copia y completa la siguiente plantilla y pégala en tu chat con el agente cuando inicies un nuevo desarrollo:
|
||||
|
||||
```
|
||||
Implementa una pantalla en Next.js siguiendo AGENT.md, docs/guia-4-implemented-pattern.md y las reglas operacionales.
|
||||
|
||||
### 1. Contexto y Enrutamiento
|
||||
- Nombre técnico: [ej. Dashboard de Transferencias]
|
||||
- URL Path: [ej. /dashboard/transferir]
|
||||
- Nivel de Seguridad: [ej. Protegida por middleware serverRequireAuth]
|
||||
|
||||
### 2. Especificación de UI (F Figma / daisyUI)
|
||||
- Estructura visual requerida: [ej. Panel lateral, formulario central de 3 campos, tabla resumen]
|
||||
- Componentes daisyUI a usar: [ej. input, select, button, card, modal]
|
||||
- Estado del cliente: [ej. Registrar 'montoTransferido' en Zustand store lib/auth/client/useAuthStore.ts]
|
||||
|
||||
### 3. Integración de APIs (Backend Downstream)
|
||||
- Endpoint a consumir: [ej. POST /api/transferencias o POST directo a backend Spring Boot]
|
||||
- Cifrado requerido: [ej. Cifrar 'cuentaOrigen' y 'cuentaDestino' usando lib/cypher/encrypt.ts]
|
||||
- Headers obligatorios: [ej. traceId, appId]
|
||||
|
||||
### 4. Activación de Skills (Gobernanza)
|
||||
- Activar 'security-best-practices': para validación de flags de cookies y sanitización.
|
||||
- Activar 'test-driven-development': para generar la suite de pruebas unitarias en Vitest.
|
||||
- Activar 'code-review-excellence': para auditar la implementación final antes del PR.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Ejemplo Práctico
|
||||
|
||||
### Prompt enviado al agente:
|
||||
```
|
||||
Implementa una pantalla en Next.js siguiendo AGENT.md, docs/guia-4-implemented-pattern.md y las reglas operacionales.
|
||||
|
||||
### 1. Contexto y Enrutamiento
|
||||
- Nombre técnico: Dashboard de Resumen Financiero
|
||||
- URL Path: /dashboard
|
||||
- Nivel de Seguridad: Protegida por middleware serverRequireAuth
|
||||
|
||||
### 2. Especificación de UI (F Figma / daisyUI)
|
||||
- Estructura visual: Panel con 3 tarjetas de resumen (Saldo Ahorros, Saldo Corriente, Tarjeta de Crédito) y una tabla con los últimos 5 movimientos.
|
||||
- Componentes daisyUI: card, table, badge, dropdown.
|
||||
- Estado del cliente: Ninguno persistente.
|
||||
|
||||
### 3. Integración de APIs (Backend Downstream)
|
||||
- Endpoint a consumir: GET /api/cuentas (Next.js Route Handler que propaga credenciales de cookie HTTP-only y llama al Spring Boot backend /cuentas).
|
||||
- Cifrado requerido: Cifrar el balance y el número de cuenta al mostrar.
|
||||
|
||||
### 4. Activación de Skills (Gobernanza)
|
||||
- Activar 'code-review-excellence' al finalizar para verificar que no haya directivas de localStorage u omisiones de auth.
|
||||
```
|
||||
67
docs/guia-v5-advanced-ai-architecture.md
Normal file
67
docs/guia-v5-advanced-ai-architecture.md
Normal file
@ -0,0 +1,67 @@
|
||||
# Guía V5 — Arquitectura Cognitiva Avanzada
|
||||
## Planos y uso de Engram Cloud, Graphify y la orquestación Multi-Agente L1 en frontend-ai
|
||||
|
||||
> **Propósito:** Esta guía documenta la infraestructura cognitiva avanzada integrada en el proyecto, permitiendo a los desarrolladores y agentes operar con la máxima eficiencia, persistencia de memoria y navegación de contexto veloz.
|
||||
|
||||
---
|
||||
|
||||
## 1. Engram: Memoria Persistente Compartida
|
||||
|
||||
**Engram** es un servidor MCP local y en la nube que gestiona una base de datos SQLite con capacidades de búsqueda de texto completo (FTS5 + BM25). Evita que los agentes autónomos de IA sufran pérdida de memoria a largo plazo entre sesiones y permite compartir aprendizajes técnicos y decisiones arquitectónicas entre desarrolladores.
|
||||
|
||||
### Flujo de Sincronización en la Nube:
|
||||
|
||||
```
|
||||
[Máquina Dev A] [Máquina Dev B]
|
||||
- mem_save("Kong encriptación fixed") - engram sync --cloud
|
||||
- engram sync --cloud - mem_search("Kong")
|
||||
│ ▲
|
||||
▼ │
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ https://engram.pranical.com │ (Servidor Central)
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Comandos Clave:
|
||||
```bash
|
||||
# Registrar el servidor de sincronización (una sola vez)
|
||||
engram cloud config --server https://engram.pranical.com
|
||||
|
||||
# Matricular el repositorio de frontend-ai
|
||||
engram cloud enroll frontend-ai
|
||||
|
||||
# Forzar sincronización de memorias
|
||||
engram sync --cloud --project frontend-ai
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Graphify: Navegación de Código Basada en Grafos
|
||||
|
||||
En proyectos medianos o grandes (más de 40 archivos), leer archivos de código línea por línea para entender la arquitectura consume una gran cantidad de tokens de contexto y ralentiza la respuesta del agente.
|
||||
|
||||
**Graphify** analiza la base de código, layouts de Next.js, componentes y API routes, y genera un **Knowledge Graph** semántico persistido en `graphify-out/graph.json` y visualizable en `graphify-out/graph.html`.
|
||||
|
||||
### Instalación e Integración:
|
||||
```bash
|
||||
# Generar el grafo semántico del repositorio
|
||||
graphify .
|
||||
|
||||
# Instalar y enlazar el MCP local en el agente Antigravity
|
||||
graphify antigravity install
|
||||
```
|
||||
|
||||
Tras la instalación, el agente consultará `graphify-out/GRAPH_REPORT.md` o interrogará el grafo directamente mediante el MCP, permitiéndole mapear dependencias de componentes (~71 veces más rápido y barato en uso de tokens).
|
||||
|
||||
*Nota: Asegúrate de agregar la carpeta `graphify-out/` a tu archivo `.gitignore`. Cada desarrollador debe generarla localmente.*
|
||||
|
||||
---
|
||||
|
||||
## 3. Orquestación Multi-Agente Nivel 1 (Agent Teams Lite)
|
||||
|
||||
Cuando realizas tareas complejas que involucran múltiples archivos en paralelo (ej. crear el Layout global de navegación y los Server Components de 3 subpáginas), el agente principal delegará subtareas de forma autónoma a **subagentes de ejecución independientes**.
|
||||
|
||||
### Ventajas de la Orquestación L1:
|
||||
- **Especialización:** Un subagente escribe la UI pura (HTML/CSS) mientras otro codifica las pruebas de integración en Vitest.
|
||||
- **Eficiencia de Contexto:** Cada subagente carga únicamente el contexto exacto (`standards/` y `skills/` específicos de su tarea), minimizando errores técnicos y costos.
|
||||
- **Gobernanza:** El agente principal actúa como un director técnico (HITL - Human in the Loop), recopilando los entregables de los subagentes, auditándolos con `code-review-excellence`, y pidiendo confirmación al desarrollador antes de mergear al repositorio.
|
||||
60
lib/auth/client/useAuthStore.ts
Normal file
60
lib/auth/client/useAuthStore.ts
Normal file
@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
userName: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
userType: 'master' | 'operador' | 'cliente';
|
||||
status: 'activo' | 'bloqueado' | 'suspendido' | 'inactivo' | 'reiniciado';
|
||||
}
|
||||
|
||||
export interface AuthStoreState {
|
||||
user: User | null;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
requires2FA: boolean;
|
||||
tempUserName: string | null;
|
||||
|
||||
setUser: (user: User | null) => void;
|
||||
setRequires2FA: (requires: boolean, userName?: string | null) => void;
|
||||
logout: () => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthStoreState>((set) => ({
|
||||
user: null,
|
||||
isLoading: false,
|
||||
isAuthenticated: false,
|
||||
requires2FA: false,
|
||||
tempUserName: null,
|
||||
|
||||
setUser: (user) =>
|
||||
set({
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
requires2FA: false,
|
||||
tempUserName: null,
|
||||
isLoading: false,
|
||||
}),
|
||||
|
||||
setRequires2FA: (requires, userName = null) =>
|
||||
set({
|
||||
requires2FA: requires,
|
||||
tempUserName: userName,
|
||||
isLoading: false,
|
||||
}),
|
||||
|
||||
logout: () =>
|
||||
set({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
requires2FA: false,
|
||||
tempUserName: null,
|
||||
isLoading: false,
|
||||
}),
|
||||
|
||||
setLoading: (loading) => set({ isLoading: loading }),
|
||||
}));
|
||||
31
lib/crypto/cipher.ts
Normal file
31
lib/crypto/cipher.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const algorithm = 'aes-256-cbc';
|
||||
const ENCRYPTION_KEY = process.env.KONG_SECRET_KEY || '6275666665726b6579646576656c6f706d656e746b6579333263686172733132'; // 32 hex bytes placeholder
|
||||
const ENCRYPTION_IV = '1234567890abcdef1234567890abcdef'; // 16 hex bytes IV placeholder
|
||||
|
||||
export function encrypt(text: string): string {
|
||||
try {
|
||||
const key = Buffer.from(ENCRYPTION_KEY.substring(0, 64), 'hex');
|
||||
const iv = Buffer.from(ENCRYPTION_IV.substring(0, 32), 'hex');
|
||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
return encrypted;
|
||||
} catch (err) {
|
||||
return text; // placeholder fallback
|
||||
}
|
||||
}
|
||||
|
||||
export function decrypt(encrypted: string): string {
|
||||
try {
|
||||
const key = Buffer.from(ENCRYPTION_KEY.substring(0, 64), 'hex');
|
||||
const iv = Buffer.from(ENCRYPTION_IV.substring(0, 32), 'hex');
|
||||
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
} catch (err) {
|
||||
return encrypted; // placeholder fallback
|
||||
}
|
||||
}
|
||||
5
lib/cypher/decrypt.ts
Normal file
5
lib/cypher/decrypt.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { decrypt } from '../crypto/cipher';
|
||||
|
||||
export function kongDecrypt(encrypted: string): string {
|
||||
return decrypt(encrypted);
|
||||
}
|
||||
5
lib/cypher/encrypt.ts
Normal file
5
lib/cypher/encrypt.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { encrypt } from '../crypto/cipher';
|
||||
|
||||
export function kongEncrypt(text: string): string {
|
||||
return encrypt(text);
|
||||
}
|
||||
34
lib/logger/client-logger.ts
Normal file
34
lib/logger/client-logger.ts
Normal file
@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { apiClient } from "../utils/api-client";
|
||||
|
||||
export type LogLevel = 'info' | 'warn' | 'error';
|
||||
|
||||
interface LogPayload {
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
async function sendLogToServer(payload: LogPayload): Promise<void> {
|
||||
try {
|
||||
await apiClient.post('/api/logs', payload, {
|
||||
timeoutMs: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to send log to server:', error);
|
||||
console.log(`[CLIENT ${payload.level.toUpperCase()}]`, payload.message, payload.meta);
|
||||
}
|
||||
}
|
||||
|
||||
export const logInfo = (message: string, meta?: Record<string, any>) => {
|
||||
sendLogToServer({ level: 'info', message, meta });
|
||||
};
|
||||
|
||||
export const logWarn = (message: string, meta?: Record<string, any>) => {
|
||||
sendLogToServer({ level: 'warn', message, meta });
|
||||
};
|
||||
|
||||
export const logError = (message: string, meta?: Record<string, any>) => {
|
||||
sendLogToServer({ level: 'error', message, meta });
|
||||
};
|
||||
71
lib/logger/server-logger.ts
Normal file
71
lib/logger/server-logger.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const LOG_FILE_PATH = process.env.LOG_FILE_PATH || './logs/app.log';
|
||||
const ENABLE_LOGS = process.env.ENABLE_SERVER_LOGS !== 'false';
|
||||
|
||||
export type LogLevel = 'info' | 'warn' | 'error';
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
function ensureLogDirectory() {
|
||||
const logDir = path.dirname(LOG_FILE_PATH);
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function formatLogEntry(entry: LogEntry): string {
|
||||
const metaStr = entry.meta ? ` | ${JSON.stringify(entry.meta)}` : '';
|
||||
return `[${entry.timestamp}] [${entry.level.toUpperCase()}] ${entry.message}${metaStr}\n`;
|
||||
}
|
||||
|
||||
export function writeLog(level: LogLevel, message: string, meta?: Record<string, any>) {
|
||||
if (!ENABLE_LOGS) return;
|
||||
|
||||
try {
|
||||
ensureLogDirectory();
|
||||
|
||||
const entry: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
meta,
|
||||
};
|
||||
|
||||
const logLine = formatLogEntry(entry);
|
||||
fs.appendFileSync(LOG_FILE_PATH, logLine, 'utf8');
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[${level.toUpperCase()}]`, message, meta || '');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to write log:', error);
|
||||
console.log(`[${level.toUpperCase()}]`, message, meta || '');
|
||||
}
|
||||
}
|
||||
|
||||
export async function logServerAction(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
meta?: Record<string, any>
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
writeLog(level, message, meta);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export const logInfo = (message: string, meta?: Record<string, any>) =>
|
||||
writeLog('info', message, meta);
|
||||
|
||||
export const logWarn = (message: string, meta?: Record<string, any>) =>
|
||||
writeLog('warn', message, meta);
|
||||
|
||||
export const logError = (message: string, meta?: Record<string, any>) =>
|
||||
writeLog('error', message, meta);
|
||||
82
lib/utils/api-client.ts
Normal file
82
lib/utils/api-client.ts
Normal file
@ -0,0 +1,82 @@
|
||||
export interface ApiResponse<T = any> {
|
||||
error?: string;
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
requires2FA?: boolean;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private getBaseURL(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
// Server-side calls downstream Spring Boot
|
||||
return process.env.API_URL_SERVER || 'http://localhost:8080';
|
||||
}
|
||||
// Client-side calls Next.js API Routes proxy
|
||||
return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api';
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
method: string,
|
||||
endpoint: string,
|
||||
body?: any,
|
||||
config?: RequestInit & { timeoutMs?: number }
|
||||
): Promise<any> {
|
||||
const baseURL = this.getBaseURL();
|
||||
const url = `${baseURL}${endpoint}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(config?.headers as any),
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), config?.timeoutMs || 10000);
|
||||
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
...config,
|
||||
};
|
||||
|
||||
if (body !== undefined) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(url, options);
|
||||
clearTimeout(id);
|
||||
|
||||
if (!res.ok && res.status !== 400 && res.status !== 401 && res.status !== 403) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
} catch (err: any) {
|
||||
clearTimeout(id);
|
||||
if (err.name === 'AbortError') {
|
||||
throw new Error('Request timeout');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
get<T>(endpoint: string, config?: RequestInit & { timeoutMs?: number }) {
|
||||
return this.request<T>('GET', endpoint, undefined, config);
|
||||
}
|
||||
|
||||
post<T>(endpoint: string, data?: any, config?: RequestInit & { timeoutMs?: number }) {
|
||||
return this.request<T>('POST', endpoint, data, config);
|
||||
}
|
||||
|
||||
put<T>(endpoint: string, data?: any, config?: RequestInit & { timeoutMs?: number }) {
|
||||
return this.request<T>('PUT', endpoint, data, config);
|
||||
}
|
||||
|
||||
delete<T>(endpoint: string, config?: RequestInit & { timeoutMs?: number }) {
|
||||
return this.request<T>('DELETE', endpoint, undefined, config);
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
0
logs/app.log
Normal file
0
logs/app.log
Normal file
18
mocks/mappings/auth-me.json
Normal file
18
mocks/mappings/auth-me.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/api/auth/me"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": {
|
||||
"id": "123",
|
||||
"name": "Juan Banesco",
|
||||
"email": "juan@banesco.com",
|
||||
"sessionReference": "c983d7aa-c81b-4f99-923f-e14b512c0199"
|
||||
}
|
||||
}
|
||||
}
|
||||
24
mocks/mappings/cuentas.json
Normal file
24
mocks/mappings/cuentas.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/api/cuentas"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": {
|
||||
"statusResponse": {
|
||||
"status": "ok",
|
||||
"statusCode": "200",
|
||||
"message": "Operación exitosa",
|
||||
"traceId": "c983d7aa-c81b-4f99-923f-e14b512c0199"
|
||||
},
|
||||
"cuentas": [
|
||||
{ "id": "cta-01", "numero": "0102-1234-56-1234567890", "saldo": 5430.50, "tipo": "Corriente" },
|
||||
{ "id": "cta-02", "numero": "0102-8877-99-9876543210", "saldo": 12500.00, "tipo": "Ahorros" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
21
mocks/mappings/login-stage1-otp.json
Normal file
21
mocks/mappings/login-stage1-otp.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/auth/login",
|
||||
"bodyPatterns": [
|
||||
{
|
||||
"absent": "softToken"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": {
|
||||
"requires2FA": true,
|
||||
"message": "Ingresa el código de autenticación"
|
||||
}
|
||||
}
|
||||
}
|
||||
32
mocks/mappings/login-stage2-failed-blocked.json
Normal file
32
mocks/mappings/login-stage2-failed-blocked.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/auth/login",
|
||||
"bodyPatterns": [
|
||||
{
|
||||
"matchesJsonPath": "$.softToken"
|
||||
},
|
||||
{
|
||||
"equalToJson": "{\"softToken\": \"000000\"}",
|
||||
"ignoreExtraElements": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": {
|
||||
"success": false,
|
||||
"accountStatus": "bloqueado",
|
||||
"user": {
|
||||
"id": "usr-123",
|
||||
"userName": "juanbanesco",
|
||||
"name": "Juan Banesco",
|
||||
"userType": "master",
|
||||
"status": "bloqueado"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
mocks/mappings/login-stage2-failed-warn.json
Normal file
24
mocks/mappings/login-stage2-failed-warn.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/auth/login",
|
||||
"bodyPatterns": [
|
||||
{
|
||||
"matchesJsonPath": "$.softToken"
|
||||
},
|
||||
{
|
||||
"matchesJsonPath": "$[?(@.softToken != '123456' && @.softToken != '000000')]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"status": 401,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": {
|
||||
"error": "Invalid credentials",
|
||||
"message": "Invalid Soft Token | juanbanesco"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
mocks/mappings/login-stage2-success.json
Normal file
35
mocks/mappings/login-stage2-success.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/auth/login",
|
||||
"bodyPatterns": [
|
||||
{
|
||||
"matchesJsonPath": "$.softToken"
|
||||
},
|
||||
{
|
||||
"equalToJson": "{\"softToken\": \"123456\"}",
|
||||
"ignoreExtraElements": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": {
|
||||
"success": true,
|
||||
"token": "access-token-1234-jwt-placeholder-string",
|
||||
"refreshToken": "refresh-token-5678-jwt-placeholder-string",
|
||||
"expiresIn": 3600,
|
||||
"user": {
|
||||
"id": "usr-123",
|
||||
"userName": "juanbanesco",
|
||||
"name": "Juan Banesco",
|
||||
"email": "juan@banesco.com",
|
||||
"userType": "master",
|
||||
"status": "activo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
mocks/mappings/transferencias.json
Normal file
21
mocks/mappings/transferencias.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/api/transferencias"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": {
|
||||
"statusResponse": {
|
||||
"status": "ok",
|
||||
"statusCode": "200",
|
||||
"message": "Transferencia realizada con éxito",
|
||||
"traceId": "c983d7aa-c81b-4f99-923f-e14b512c0199"
|
||||
},
|
||||
"transactionId": "TXN-9988776655"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
next-env.d.ts
vendored
Normal file
6
next-env.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
7
next.config.ts
Normal file
7
next.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6317
package-lock.json
generated
Normal file
6317
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "ibanking-ai-api",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.2.6",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.12.2",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
26
setup.ps1
Normal file
26
setup.ps1
Normal file
@ -0,0 +1,26 @@
|
||||
# setup.ps1
|
||||
Write-Output "Iniciando inicializacion de entorno..."
|
||||
|
||||
# 1. Copiar archivo de entorno si no existe
|
||||
if (!(Test-Path ".env.local")) {
|
||||
Write-Output "Creando .env.local desde plantilla..."
|
||||
Copy-Item ".env.local.template" ".env.local"
|
||||
} else {
|
||||
Write-Output "env.local ya existe."
|
||||
}
|
||||
|
||||
# 2. Instalar dependencias del proyecto
|
||||
if (Test-Path "package.json") {
|
||||
Write-Output "Instalando dependencias de Node.js..."
|
||||
npm install
|
||||
}
|
||||
|
||||
# 3. Crear carpetas de logs
|
||||
if (!(Test-Path "logs")) {
|
||||
$null = New-Item -ItemType Directory -Path "logs" -Force
|
||||
}
|
||||
if (!(Test-Path "logs/app.log")) {
|
||||
$null = New-Item -ItemType File -Path "logs/app.log" -Force
|
||||
}
|
||||
|
||||
Write-Output "Inicializacion completa!"
|
||||
48
setup.sh
Normal file
48
setup.sh
Normal file
@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# =========================================================================
|
||||
# setup.sh — Inicialización automática de entorno ibanking-ai-api (Mac/Linux)
|
||||
# =========================================================================
|
||||
|
||||
echo "========================================================="
|
||||
echo "🚀 Iniciando inicialización de entorno ibanking-ai-api..."
|
||||
echo "========================================================="
|
||||
|
||||
# 1. Copiar archivo de entorno si no existe
|
||||
if [ ! -f .env.local ]; then
|
||||
echo "Creating .env.local from template..."
|
||||
cp .env.local.template .env.local
|
||||
echo "✅ Archivo .env.local configurado correctamente."
|
||||
else
|
||||
echo "ℹ️ .env.local ya existe. Se omite la copia."
|
||||
fi
|
||||
|
||||
# 2. Instalar dependencias del proyecto
|
||||
if [ -f package.json ]; then
|
||||
echo "Instalando dependencias de Node.js..."
|
||||
npm install
|
||||
echo "✅ Dependencias de Node.js instaladas."
|
||||
fi
|
||||
|
||||
# 3. Inicializar e instalar Graphify y Engram MCP Servers
|
||||
echo "Configurando herramientas de ingeniería AI-Native..."
|
||||
|
||||
if ! command -v uv &> /dev/null; then
|
||||
echo "Instalando gestor de paquetes python 'uv'..."
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
fi
|
||||
|
||||
echo "Instalando/Actualizando Graphify..."
|
||||
uv tool install graphifyy && graphify install
|
||||
|
||||
echo "Integrando Graphify con el agente Antigravity..."
|
||||
graphify antigravity install
|
||||
|
||||
# 4. Crear carpeta de logs local
|
||||
mkdir -p logs
|
||||
touch logs/app.log
|
||||
|
||||
echo "========================================================="
|
||||
echo "🎉 ¡Todo listo! Ejecuta 'docker-compose up -d' para mocks"
|
||||
echo "y luego 'npm run dev' para iniciar el servidor Next.js."
|
||||
echo "========================================================="
|
||||
125
standards/01-log-tracing.md
Normal file
125
standards/01-log-tracing.md
Normal file
@ -0,0 +1,125 @@
|
||||
# 01 — Trazas de Log y Centralización
|
||||
## Estándar obligatorio de logging para Server Components, Client Components y API Routes
|
||||
|
||||
> **Uso:** Todo flujo, acción crítica del usuario (login, logout, accesos denegados) o error de integración DEBE registrar un log con el formato estándar del proyecto.
|
||||
|
||||
---
|
||||
|
||||
## 1. Arquitectura de Logs de Frontend
|
||||
|
||||
La aplicación utiliza un sistema centralizado de logs que asegura que todas las trazas (tanto del cliente como del servidor) queden consolidadas en el servidor en el archivo `logs/app.log`.
|
||||
|
||||
```
|
||||
[Client Component] (use client)
|
||||
│ (llama a client-logger)
|
||||
▼
|
||||
POST /api/logs
|
||||
│
|
||||
▼
|
||||
[Route Handler] (app/api/logs/route.ts)
|
||||
│
|
||||
▼
|
||||
[Server Logger] (lib/logger/server-logger.ts) ──► Escribe en logs/app.log
|
||||
▲
|
||||
│ (llamada directa)
|
||||
[Server Component / API Route]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Formato de las Trazas
|
||||
|
||||
El formato estándar del log es una sola línea estructurada para facilitar búsquedas y parseos:
|
||||
|
||||
```
|
||||
[<Timestamp-ISO>] [<LEVEL>] <Mensaje-Descriptivo> | <Metadata-JSON>
|
||||
```
|
||||
|
||||
### Niveles Aceptados:
|
||||
- **`[INFO]`**: Acciones exitosas y flujos de negocio normales (ej. Login exitoso, Logout).
|
||||
- **`[WARN]`**: Acciones sospechosas, intentos fallidos o no autorizados (ej. Login fallido, Acceso denegado a dashboard).
|
||||
- **`[ERROR]`**: Fallos técnicos críticos, excepciones de red o integración (ej. Backend Spring Boot caído, fallo de descompresión).
|
||||
|
||||
---
|
||||
|
||||
## 3. Uso Práctico
|
||||
|
||||
### 3.1 Desde Server Components y API Route Handlers
|
||||
En el servidor, importamos y llamamos directamente al `server-logger`:
|
||||
|
||||
```typescript
|
||||
import { logInfo, logWarn, logError } from '@/lib/logger/server-logger';
|
||||
|
||||
// Registro de flujo regular (INFO)
|
||||
logInfo('Usuario inició sesión exitosamente', { userId: '123', email: 'juan@banesco.com' });
|
||||
|
||||
// Intento de acceso fallido (WARN)
|
||||
logWarn('Acceso no autorizado a dashboard', { redirectTo: '/login', path: '/dashboard' });
|
||||
|
||||
// Fallo de backend (ERROR)
|
||||
logError('Error al conectar con el backend de Spring Boot', { error: 'ECONNREFUSED', endpoint: '/api/auth/me' });
|
||||
```
|
||||
|
||||
### 3.2 Desde Client Components ('use client')
|
||||
En el cliente, importamos `client-logger` que se encarga asíncronamente de enviar la traza al backend local mediante `POST /api/logs`:
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
import { logInfo, logWarn, logError } from '@/lib/logger/client-logger';
|
||||
|
||||
function FormularioTransferencia() {
|
||||
const handleConfirmar = () => {
|
||||
// Logueamos la acción en el cliente (viajará al servidor automáticamente)
|
||||
logInfo('Click en confirmar transferencia', { monto: 1500, moneda: 'USD' });
|
||||
};
|
||||
|
||||
const handleValidacionFallida = () => {
|
||||
logWarn('Validación de saldo insuficiente en cliente', { saldoDisponible: 200, montoRequerido: 1500 });
|
||||
};
|
||||
|
||||
return (
|
||||
// ... JSX
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Estructura de la API Route de Logs (`app/api/logs/route.ts`)
|
||||
|
||||
La API `/api/logs` es el proxy encargado de recibir los logs del cliente. Debe validar que la petición no esté vacía y pasarla inmediatamente al `server-logger`:
|
||||
|
||||
```typescript
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { logInfo, logWarn, logError } from '@/lib/logger/server-logger';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { level, message, metadata } = await req.json();
|
||||
|
||||
switch (level.toUpperCase()) {
|
||||
case 'INFO':
|
||||
logInfo(message, metadata);
|
||||
break;
|
||||
case 'WARN':
|
||||
logWarn(message, metadata);
|
||||
break;
|
||||
case 'ERROR':
|
||||
logError(message, metadata);
|
||||
break;
|
||||
default:
|
||||
logInfo(`[CLIENT-LOG] ${message}`, metadata);
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: 'success' }, { status: 200 });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Fallo al procesar logs en el proxy' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Reglas de Seguridad en Logs
|
||||
- **NUNCA** guardes contraseñas de usuario, tokens JWT, llaves secretas o números de tarjeta de crédito (PAN) completos en los logs.
|
||||
- Aplica mascarado de datos sensibles en el metadata (ej. `{"email": "ju***@banesco.com"}`).
|
||||
107
standards/02-security-tracing.md
Normal file
107
standards/02-security-tracing.md
Normal file
@ -0,0 +1,107 @@
|
||||
# 02 — Seguridad, Autenticación y Cifrado
|
||||
## Estándar obligatorio de cookies HTTP-only, middleware Next.js y encriptación Kong
|
||||
|
||||
> **Uso:** Esta guía define las directrices y mecanismos de protección obligatorios para evitar fugas de información, proteger sesiones de usuario y asegurar transacciones.
|
||||
|
||||
---
|
||||
|
||||
## 1. Protección de Rutas (Autenticación Server-Side)
|
||||
|
||||
El frontend protege páginas y layouts directamente en el servidor utilizando el middleware `serverRequireAuth` antes de renderizar la interfaz. Esto previene el parpadeo de pantallas ("flash of unauthenticated content") y asegura que ningún Server Component exponga datos no autorizados.
|
||||
|
||||
### 1.1 Proteger un Server Component (Page / Layout):
|
||||
```typescript
|
||||
import { serverRequireAuth } from '@/lib/auth/middleware';
|
||||
|
||||
export default async function DashboardPage() {
|
||||
// 1. Invocar obligatoriamente en la cabecera
|
||||
const user = await serverRequireAuth(); // Redirige a /login si falla la sesión
|
||||
|
||||
// 2. Si pasa, renderizamos de forma segura con los datos del usuario
|
||||
return (
|
||||
<main className="p-6">
|
||||
<h1>Bienvenido al Portal Seguro, {user.name}</h1>
|
||||
<p>ID de usuario verificado: {user.id}</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Chequeo de Autenticación Opcional:
|
||||
Si deseas validar si el usuario está autenticado pero sin forzar la redirección (ej. en la Landing Page pública):
|
||||
```typescript
|
||||
import { checkAuth } from '@/lib/auth/middleware';
|
||||
|
||||
export default async function LandingPage() {
|
||||
const { isAuthenticated, user } = await checkAuth();
|
||||
|
||||
return (
|
||||
<nav>
|
||||
{isAuthenticated ? (
|
||||
<span>Hola, {user?.name}</span>
|
||||
) : (
|
||||
<a href="/login">Iniciar Sesión</a>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Almacenamiento Seguro de Sesión (HTTP-only Cookies)
|
||||
|
||||
Queda **estrictamente prohibido** almacenar tokens JWT, contraseñas o datos de sesión en `localStorage` o `sessionStorage`. El navegador es vulnerable a ataques XSS y robo de tokens en estos alcances.
|
||||
|
||||
### Directrices de Cookies:
|
||||
1. **`access_token`** y **`refresh_token`** deben configurarse en el backend o en el Route Handler de Next.js como:
|
||||
- `httpOnly: true` (Evita acceso por scripts JS).
|
||||
- `secure: true` (Viaja solo por HTTPS — desactivado solo en localhost).
|
||||
- `sameSite: 'strict'` (Mitigación completa de CSRF).
|
||||
- `path: '/'` (Válido para todo el dominio).
|
||||
|
||||
---
|
||||
|
||||
## 3. Cifrado Perimetral de Datos (Kong Gateway)
|
||||
|
||||
Para cualquier dato altamente sensible en tránsito en el cliente (como números de cuenta de ahorros, saldos o datos personales) que no queramos que sea interceptado o inspeccionado en las herramientas de desarrollo del navegador, aplicaremos el cifrado Kong perimetral.
|
||||
|
||||
Utilizamos las funciones integradas en `lib/cypher/encrypt.ts` y `lib/cypher/decrypt.ts`:
|
||||
|
||||
### 3.1 Cifrar datos en el cliente antes de enviar:
|
||||
```typescript
|
||||
import { kongEncrypt } from '@/lib/cypher/encrypt';
|
||||
|
||||
function ConfirmarTransaccion() {
|
||||
const enviarMontoSeguro = () => {
|
||||
const cuentaOrigenCifrada = kongEncrypt('0102-1234-56-1234567890');
|
||||
|
||||
// El payload viajará cifrado y será descifrado en el Gateway Kong o en el Backend
|
||||
apiCall('/api/transferencia', {
|
||||
cuentaOrigen: cuentaOrigenCifrada
|
||||
});
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Descifrar datos en el servidor/cliente tras recibirlos:
|
||||
```typescript
|
||||
import { kongDecrypt } from '@/lib/cypher/decrypt';
|
||||
|
||||
function DetalleCuenta({ cuentaEncriptada }: { cuentaEncriptada: string }) {
|
||||
// Descifrar para visualización del usuario final
|
||||
const numeroCuentaReal = kongDecrypt(cuentaEncriptada);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Número de cuenta: {numeroCuentaReal}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Reglas de Oro de Seguridad en Frontend
|
||||
- **Secrets de Entorno:** Nunca commitees archivos `.env.local` con secretos de producción reales. Las variables de servidor de Next.js (sin prefijo `NEXT_PUBLIC_`) son inaccesibles para el cliente. Úsalas para llaves privadas de cifrado o tokens de backend.
|
||||
- **Sanitización de Inputs:** Valida y sanitiza todos los datos ingresados por el usuario utilizando esquemas robustos (Zod o TypeScript estricto) antes de renderizarlos en la UI para prevenir inyecciones HTML/React.
|
||||
90
standards/03-headers-contract.md
Normal file
90
standards/03-headers-contract.md
Normal file
@ -0,0 +1,90 @@
|
||||
# 03 — Contrato de Headers y Trazabilidad (traceId)
|
||||
## Contrato de comunicación entre Cliente, Next.js Server Components y APIs de Downstream
|
||||
|
||||
> **Uso:** Esta guía detalla cómo estructurar y propagar los headers de autenticación e identificación del dispositivo, asegurando la trazabilidad del `traceId` de extremo a extremo.
|
||||
|
||||
---
|
||||
|
||||
## 1. El Ciclo de Vida del `traceId` (Request Correlation)
|
||||
|
||||
El `traceId` es la clave de correlación única generada por el cliente que permite rastrear una petición desde el navegador del usuario, pasando por Next.js App Router, Kong Gateway, hasta llegar a las APIs de Spring Boot y bases de datos.
|
||||
|
||||
En nuestro Internet Banking Seguro:
|
||||
- El `traceId` se toma del valor `device.deviceSessionReference` enviado por el cliente o del ID de sesión generado por Next.js Middleware.
|
||||
- Se propaga obligatoriamente en todas las cabeceras de las llamadas de servicios HTTP a APIs downstream.
|
||||
|
||||
---
|
||||
|
||||
## 2. Matriz de Headers Requeridos
|
||||
|
||||
Cuando Next.js Server Components o Route Handlers realizan peticiones al backend (Spring Boot), deben incluir el siguiente contrato de headers bancarios:
|
||||
|
||||
| Header | Tipo | Propósito | Fuente en Frontend |
|
||||
|---|---|---|---|
|
||||
| `Authorization` | Bearer Token | Autenticación del usuario final | Extraído de la cookie HTTP-only `access_token` |
|
||||
| `traceId` | UUID / String | Correlación única de petición | `deviceSessionReference` o UUID de middleware |
|
||||
| `appId` | String | Identificación del cliente (ej. portal web) | Variable de entorno (.env) del servidor |
|
||||
| `agencyCode` | String | Identificación de la agencia física | Extraído del perfil del usuario (Zustand/Cookie) |
|
||||
| `customerReferenceFintechId` | String | Identificación del cliente fintech | Extraído del JWT decodificado en Next.js server |
|
||||
|
||||
---
|
||||
|
||||
## 3. Ejemplo Práctico de Propagación de Headers
|
||||
|
||||
Al consumir el backend desde una llamada de servidor en Next.js (ej. en una API Route o Server Action):
|
||||
|
||||
```typescript
|
||||
import { cookies } from 'next/headers';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export async function fetchCuentasUsuario(deviceSessionId: string) {
|
||||
const cookieStore = cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
if (!token) {
|
||||
throw new Error('No hay sesión activa para propagar credenciales');
|
||||
}
|
||||
|
||||
// Estructuramos el contrato de headers obligatorios
|
||||
const headers = new Headers({
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'traceId': deviceSessionId,
|
||||
'appId': process.env.API_APP_ID || 'PORTAL_WEB_SAFE',
|
||||
'customerReferenceFintechId': 'FT-1002394', // Extraído previamente del JWT
|
||||
});
|
||||
|
||||
const response = await fetch(`${process.env.API_URL_SERVER}/cuentas`, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
next: { revalidate: 60 } // Cache inteligente
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fallo en backend: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Respuestas del Servidor al Cliente
|
||||
|
||||
Las API Routes locales de Next.js (`/app/api/...`) deben encapsular la respuesta al cliente siguiendo una estructura coherente y propagando el `traceId` de vuelta para facilitar la depuración:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusResponse": {
|
||||
"status": "ok",
|
||||
"statusCode": "200",
|
||||
"message": "Operación exitosa",
|
||||
"traceId": "c983d7aa-c81b-4f99-923f-e14b512c0199"
|
||||
},
|
||||
"data": {
|
||||
"cuentas": [
|
||||
{ "id": "1", "saldo": 5000 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
74
standards/04-configmap-management.md
Normal file
74
standards/04-configmap-management.md
Normal file
@ -0,0 +1,74 @@
|
||||
# 04 — Gestión de Variables de Entorno y Configuración
|
||||
## Estándar obligatorio de variables públicas, privadas y archivos .env en Next.js
|
||||
|
||||
> **Uso:** El manejo inadecuado de claves de entorno puede exponer información confidencial en el navegador del cliente. Esta guía define cómo gestionar de forma segura e incremental las configuraciones en Next.js.
|
||||
|
||||
---
|
||||
|
||||
## 1. Alcance de Variables: Privadas vs Públicas
|
||||
|
||||
Next.js por defecto protege todas las variables cargadas en el proceso de Node.js. El cliente (navegador) no tiene forma de leerlas a menos que se expongan de manera explícita.
|
||||
|
||||
### 1.1 Variables del Servidor (Privadas)
|
||||
No llevan prefijo. Solo están disponibles en **Server Components, API Routes, Middleware, e Inbound/Outbound logs del lado del servidor**.
|
||||
* **Ejemplos:** Llaves secretas de cifrado Kong, URLs directas de red de Spring Boot, contraseñas de servicio.
|
||||
* *NUNCA* intentes acceder a ellas desde componentes declarados con `use client`. Retornarán `undefined`.
|
||||
|
||||
### 1.2 Variables del Cliente (Públicas)
|
||||
Deben llevar **obligatoriamente** el prefijo `NEXT_PUBLIC_`. Next.js las inyecta en el bundle de Javascript durante el build, haciéndolas accesibles desde cualquier navegador.
|
||||
* **Ejemplos:** URL pública de API routes locales, Google Analytics keys, variables de tema visual.
|
||||
* *REGLA DE ORO:* Nunca asignes llaves secretas o tokens de servicio a variables con prefijo `NEXT_PUBLIC_`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Matriz de Configuración del Proyecto
|
||||
|
||||
Las variables estándar que deben estar definidas en tu entorno local son:
|
||||
|
||||
| Variable | Tipo | Propósito | Ejemplo |
|
||||
|---|---|---|---|
|
||||
| `NODE_ENV` | Privada | Define el entorno actual | `development` / `production` |
|
||||
| `API_URL_SERVER` | Privada | Endpoint directo de la red interna del backend Spring Boot | `https://api-internal.tudominio.com/api` |
|
||||
| `NEXT_PUBLIC_API_URL` | Pública | URL de la API local de Next.js para peticiones del cliente | `http://localhost:3000/api` |
|
||||
| `KONG_SECRET_KEY` | Privada | Clave de cifrado perimetral de datos | `k-secret-key-321-abc` |
|
||||
| `ENABLE_SERVER_LOGS` | Privada | Activa/Desactiva escritura en logs/app.log | `true` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Ejemplo de Archivo `.env.local`
|
||||
|
||||
Este archivo es de carácter local y **no se incluye en git** (debe estar en `.gitignore`).
|
||||
|
||||
```env
|
||||
# ==========================================
|
||||
# CONFIGURACIÓN DEL SERVIDOR (PRIVADAS)
|
||||
# ==========================================
|
||||
NODE_ENV=development
|
||||
API_URL_SERVER=http://localhost:8080/api
|
||||
KONG_SECRET_KEY=clave_secreta_para_cifrado_local_32bits
|
||||
ENABLE_SERVER_LOGS=true
|
||||
|
||||
# ==========================================
|
||||
# CONFIGURACIÓN DEL CLIENTE (PÚBLICAS)
|
||||
# ==========================================
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3000/api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Validación de Entorno
|
||||
|
||||
Para evitar que la aplicación arranque con configuraciones incompletas o corruptas que generen fallos silenciosos, se recomienda validar las variables críticas al inicializar la app (por ejemplo, en `lib/config.ts`):
|
||||
|
||||
```typescript
|
||||
// lib/config.ts
|
||||
const requiredServerEnv = ['API_URL_SERVER', 'KONG_SECRET_KEY'];
|
||||
|
||||
export function validateEnvironment() {
|
||||
const missing = requiredServerEnv.filter((key) => !process.env[key]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`[CRITICAL] Faltan variables de entorno requeridas en el servidor: ${missing.join(', ')}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
80
standards/05-client-insumos.md
Normal file
80
standards/05-client-insumos.md
Normal file
@ -0,0 +1,80 @@
|
||||
# 05 — Insumos de Frontend y Especificación de Pantalla
|
||||
## Checklist completo de lo que el equipo de diseño y producto debe proveer antes de iniciar la programación
|
||||
|
||||
> **Uso:** Antes de escribir código para cualquier página o componente de Internet Banking, el desarrollador debe rellenar esta especificación con el cliente o diseñador de interfaz.
|
||||
> Si quedan dudas de negocio, se usarán mocks provisorios que deberán reemplazarse posteriormente.
|
||||
|
||||
---
|
||||
|
||||
## §1 — Identificación y Enrutamiento de la Pantalla
|
||||
|
||||
| Insumo | Especificación | ¿Completado? |
|
||||
|---|---|---|
|
||||
| Nombre descriptivo de la pantalla | `ej. Dashboard de Cuentas` | ☐ |
|
||||
| Ruta técnica (URL Path) | `ej. /dashboard` | ☐ |
|
||||
| Nivel de Autenticación | `Pública` / `Protegida` / `Doble Factor Requerido` | ☐ |
|
||||
|
||||
---
|
||||
|
||||
## §2 — Estructura de la Interfaz (UI Mockups)
|
||||
|
||||
Adjunta el mockup de Figma o los requerimientos visuales acordados:
|
||||
|
||||
- **Layout general:** ¿Utiliza el layout principal de navegación o es una pantalla limpia (ej. Login)?
|
||||
- **Paleta y Componentes:** Confirmar que se usen componentes de **daisyUI** (Botones, Inputs, Modales, Tables) para garantizar consistencia.
|
||||
|
||||
### Elementos Críticos de la UI:
|
||||
1. **Header / Breadcrumb:** ¿Cuáles son los títulos y navegación?
|
||||
2. **Formularios e Inputs:**
|
||||
- Lista de campos con validación en cliente (ej. monto numérico, formato email).
|
||||
3. **Tablas / Listados:**
|
||||
- Columnas a mostrar (ej. Número de cuenta cifrado, Saldo real descifrado, Estado).
|
||||
|
||||
---
|
||||
|
||||
## §3 — Integración de APIs y Backend Downstream
|
||||
|
||||
Identificar todas las llamadas de red requeridas para poblar la pantalla:
|
||||
|
||||
| Acción | Método | Endpoint (Spring Boot) | Payload requerido | Respuesta esperada |
|
||||
|---|---|---|---|---|
|
||||
| `ej. Cargar saldos` | `GET` | `/cuentas` | Ninguno (Headers Auth) | Listado de Cuentas |
|
||||
| `ej. Realizar transferencia` | `POST` | `/transferencias` | `{ cuentaOrigen, monto, traceId }` | Recibo de transferencia |
|
||||
|
||||
*Nota: Cualquier dato sensible de esta tabla (monto, número de cuenta) debe marcarse para cifrado Kong perimetral antes de ser enviado.*
|
||||
|
||||
---
|
||||
|
||||
## §4 — Estado Global y Zustand Store
|
||||
|
||||
Define qué datos deben persistir en el estado del cliente durante la sesión:
|
||||
|
||||
| Variable de Estado | Tipo | Propósito | ¿Requiere persistencia? |
|
||||
|---|---|---|---|
|
||||
| `user` | Objeto | Datos de perfil del usuario logueado | ✅ Sí (SessionStorage) |
|
||||
| `theme` | String | Modo claro / oscuro | ✅ Sí (LocalStorage) |
|
||||
| `ej. cuentasCargadas` | Array | Lista temporal de cuentas en dashboard | ☐ No (Memoria) |
|
||||
|
||||
---
|
||||
|
||||
## §5 — Registro de Logs y Trazabilidad
|
||||
|
||||
Define qué eventos críticos del usuario deben quedar grabados en `logs/app.log`:
|
||||
|
||||
| Evento | Nivel de Log | Mensaje | Metadata a registrar |
|
||||
|---|---|---|---|
|
||||
| `ej. Carga de Dashboard` | `[INFO]` | `Carga de dashboard de cuentas` | `{"userId": "<user.id>"}` |
|
||||
| `ej. Click en Transferir` | `[INFO]` | `Acción de transferencia iniciada` | `{"monto": 150, "traceId": "..."}` |
|
||||
| `ej. Intento fallido` | `[WARN]` | `Intento de transferencia saldo insuficiente` | `{"montoRequerido": 5000}` |
|
||||
|
||||
---
|
||||
|
||||
## Semáforo de Readiness de Frontend antes de Iniciar
|
||||
|
||||
| Sección | Estado | ¿Bloqueante? |
|
||||
|---|---|---|
|
||||
| **§1 Enrutamiento y Auth** | ☐ Completo / ☐ Incompleto | ✅ SÍ — define la ruta del archivo Next.js |
|
||||
| **§2 Componentes visuales** | ☐ Completo / ☐ Incompleto | ✅ SÍ — previene rehacer interfaces visuales |
|
||||
| **§3 Contratos de APIs** | ☐ Completo / ☐ Incompleto | ⚠️ Parcial — se puede crear mock en Next.js Route Handler |
|
||||
| **§4 Configuración Zustand** | ☐ Completo / ☐ Incompleto | ⚠️ Parcial — se puede definir durante el desarrollo |
|
||||
| **§5 Registro de Logs** | ☐ Completo / ☐ Incompleto | ✅ SÍ — obligatorio para cumplir estándares de trazabilidad |
|
||||
96
standards/06-auth-2fa-flow.md
Normal file
96
standards/06-auth-2fa-flow.md
Normal file
@ -0,0 +1,96 @@
|
||||
# 06 — Flujo de Autenticación con Doble Factor (2FA / OTP)
|
||||
## Contrato de negocio y reglas de seguridad para el inicio de sesión seguro
|
||||
|
||||
> **Propósito:** Esta guía define las reglas de negocio estrictas, los límites de intentos, la validación de campos y el flujo de estados necesarios para implementar el inicio de sesión seguro con Segundo Factor de Autenticación (M2FA / SoftToken).
|
||||
|
||||
---
|
||||
|
||||
## 1. El Flujo de Estados del Login 2FA
|
||||
|
||||
El proceso de autenticación consta de dos fases transaccionales para evitar la exposición de credenciales y asegurar que solo dispositivos autorizados completen el acceso.
|
||||
|
||||
```
|
||||
[POST /api/auth/login]
|
||||
(userName, password, softToken?)
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
▼ ▼
|
||||
(Sin SoftToken) (Con SoftToken)
|
||||
¿Requiere 2FA? ¿Token Válido?
|
||||
┌───────┴───────┐ ┌───────┴───────┐
|
||||
▼ ▼ ▼ ▼
|
||||
[SÍ] [NO] [SÍ] [NO]
|
||||
Retornar 2FA Login Exitoso Establecer Incrementar
|
||||
Requerido (401) Establecer Cookies JWT Intento 2FA
|
||||
Cookies JWT y retornar OK (Máx 3)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Reglas de Validación de Entrada
|
||||
|
||||
Antes de realizar la llamada al servicio de autenticación, el Route Handler local de Next.js debe validar estrictamente los campos en el servidor:
|
||||
|
||||
### 2.1 Nombre de Usuario (`userName`):
|
||||
- **Obligatoriedad:** Requerido. Si está vacío: retornar código `USERNAME_REQUIRED` con el mensaje: `"Este campo es obligatorio"` (Status `400`).
|
||||
- **Longitud Mínima:** 6 caracteres. Si es menor: retornar código `USERNAME_TOO_SHORT` con el mensaje: `"Debe ser mayor o igual a seis caracteres"` (Status `400`).
|
||||
- **Longitud Máxima:** 12 caracteres. Si es mayor: retornar código `USERNAME_TOO_LONG` con el mensaje: `"El sistema solo debe permitir ingresar 12 caracteres"` (Status `400`).
|
||||
|
||||
### 2.2 Contraseña (`password`):
|
||||
- **Obligatoriedad:** Requerido. Si está vacío: retornar código `PASSWORD_REQUIRED` con el mensaje: `"Este campo es obligatorio"` (Status `400`).
|
||||
|
||||
### 2.3 Código OTP / SoftToken (`softToken`):
|
||||
- **Obligatoriedad:** Requerido únicamente en la segunda fase del flujo (cuando se envía junto al usuario y clave). Si está vacío en la fase 2: retornar código `TOKEN_REQUIRED` con el mensaje: `"Este campo es obligatorio"` (Status `400`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Control de Intentos y Bloqueos de Usuario
|
||||
|
||||
Para proteger el sistema contra ataques de fuerza bruta, el Route Handler debe llevar un control estricto de intentos fallidos en memoria y aplicar las siguientes penalizaciones:
|
||||
|
||||
### 3.1 Control de Intentos de Contraseña (Fase 1)
|
||||
- **Límite Máximo:** 3 intentos fallidos de clave.
|
||||
- **Acción al llegar al límite:** El estado del usuario pasa a **`suspendido`**.
|
||||
- **Mensaje de Error:**
|
||||
- Intentos < 3: `"Usuario o Clave Inválida"` (Status `401`).
|
||||
- Intentos = 3: `"Tu usuario ha sido suspendido. Ingresa en la opción Autogestión de Usuario"` (Status `403`).
|
||||
|
||||
### 3.2 Control de Intentos de Código OTP / SoftToken (Fase 2)
|
||||
- **Límite Máximo:** 3 intentos fallidos de token.
|
||||
- **Acción al llegar al límite:** El estado del usuario pasa a **`bloqueado`** de forma definitiva en la base de datos y se dispara la alerta de seguridad interna.
|
||||
- **Mensajes de Error por Intento:**
|
||||
- **1er Intento Fallido:** `"Código inválido"` (Status `400`).
|
||||
- **2do Intento Fallido:** `"Código inválido, al tercer intento tu usuario será bloqueado"` (Status `400`).
|
||||
- **3er Intento Fallido (Bloqueo):** `"Código inválido, tu usuario ha sido bloqueado. Por favor ingresa a la opción Autogestión de Usuario"` (Status `403`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Control de Doble Sesión Activa (`ACTIVE_SESSION`)
|
||||
|
||||
Antes de procesar las credenciales en el backend, el sistema verificará si el usuario ya tiene una sesión web activa en otro navegador.
|
||||
- **Validación:** Comparar el `x-session-id` del request con las sesiones registradas en memoria.
|
||||
- **Acción si está activo en otra sesión:** Denegar el login y retornar el mensaje:
|
||||
- `"Usuario activo en otra sesión. Por tu seguridad cierra la sesión y vuelve a ingresar tu usuario"` (Status `403`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Estados de Cuenta del Usuario
|
||||
|
||||
El backend retornará el estado de cuenta (`status` / `accountStatus`) tras procesar las credenciales. Next.js debe denegar el acceso inmediatamente si el estado no es `"activo"`:
|
||||
|
||||
| Estado de Cuenta | Acción en Next.js | Mensaje a Retornar al Cliente | Status HTTP |
|
||||
|---|---|---|---|
|
||||
| `bloqueado` | Denegar Acceso | *"Tu usuario se encuentra bloqueado. Ingresa en la opción Autogestión de Usuario"* (Si es Master: *"Usuario bloqueado. Por favor comunícate con Soporte Exterior Empresas a través del (0212-501.5500)"*) | `403` |
|
||||
| `suspendido` | Denegar Acceso | *"Tu usuario ha sido suspendido. Ingresa en la opción Autogestión de Usuario"* | `403` |
|
||||
| `inactivo` | Denegar Acceso | *"Usuario inactivo. Por favor comunícate con Soporte Empresas a través del (0212-501.5500)"* | `403` |
|
||||
| `reiniciado` | Denegar Acceso | *"Por favor ingresa a la opción Autogestión de Usuario"* | `403` |
|
||||
|
||||
---
|
||||
|
||||
## 6. Manejo Exitoso y Establecimiento de Cookies
|
||||
|
||||
Una vez superadas las credenciales y el código 2FA de forma exitosa:
|
||||
1. **Resetear Contadores:** Poner a `0` los intentos de contraseña y token de ese usuario.
|
||||
2. **Establecer Cookies HTTP-only:**
|
||||
- **`access_token`:** Token de acceso de corta duración (con flag `httpOnly`, `secure: true` en prod, `sameSite: 'strict'`, y su `maxAge` provisto por el backend).
|
||||
- **`refresh_token`:** Token de refresco de larga duración (configurado con las mismas flags y `maxAge` de 7 días).
|
||||
39
standards/README.md
Normal file
39
standards/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Standards — Guías de Implementación Frontend Segura
|
||||
## Referencia obligatoria para todo desarrollo en el proyecto frontend-ai
|
||||
|
||||
**Versión:** 1.0 · **Fecha:** 2026-05-27
|
||||
**Patrón base:** `docs/guia-4-implemented-pattern.md`
|
||||
|
||||
---
|
||||
|
||||
## Contenido
|
||||
|
||||
| # | Guía | Objetivo |
|
||||
|---|---|---|
|
||||
| [01](./01-log-tracing.md) | **Trazas de Log** | Sistema de logs centralizado, `server-logger`, `client-logger`, proxy asíncrono `/api/logs` |
|
||||
| [02](./02-security-tracing.md) | **Seguridad y Cifrado** | Autenticación server-side con `serverRequireAuth`, cookies JWT http-only, y encriptación Kong |
|
||||
| [03](./03-headers-contract.md) | **Contrato de Headers** | Cookies de sesión, traceId (`deviceSessionReference`), propagación a APIs Spring Boot |
|
||||
| [04](./04-configmap-management.md) | **Gestión de Configuración** | Estructura de archivos `.env`, variables del servidor (privadas) vs variables del cliente (`NEXT_PUBLIC_`) |
|
||||
| [05](./05-client-insumos.md) | **Insumos de Frontend** | Plantilla interactiva para rellenar especificaciones de pantalla, estado, mockups y endpoints antes de empezar |
|
||||
| [06](./06-auth-2fa-flow.md) | **Autenticación y 2FA** | Reglas de negocio del inicio de sesión, control de intentos, bloqueos de usuario y cookies http-only |
|
||||
|
||||
---
|
||||
|
||||
## Cómo usar estas guías
|
||||
|
||||
```
|
||||
INICIO DE IMPLEMENTACIÓN PANTALLA
|
||||
└─ Leer 05-client-insumos.md → recopilar con diseño/producto mockups y endpoints
|
||||
└─ Leer 03-headers-contract.md → validar traceId, cookies JWT y headers requeridos
|
||||
|
||||
DURANTE EL DESARROLLO
|
||||
└─ Leer 01-log-tracing.md → registrar logs en servidor y cliente (/api/logs)
|
||||
└─ Leer 02-security-tracing.md → aplicar middleware serverRequireAuth y encriptación Kong
|
||||
└─ Leer 04-configmap-management.md → registrar configuraciones en .env.local
|
||||
|
||||
CHECKLIST FINAL
|
||||
└─ AGENT.md §Checklist pre-entrega
|
||||
```
|
||||
|
||||
> **Regla de oro:** si tienes duda de si una lógica se ejecuta en el servidor o cliente,
|
||||
> la respuesta está en `02-security-tracing.md` y `docs/guia-4-implemented-pattern.md`.
|
||||
24
tailwind.config.ts
Normal file
24
tailwind.config.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./lib/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#00875A', // Color Verde Corporativo de BD
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("daisyui")
|
||||
],
|
||||
daisyui: {
|
||||
themes: ["light"],
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
34
tsconfig.json
Normal file
34
tsconfig.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user