feat: initialize banking project structure with login form, API client, and authentication workflow

This commit is contained in:
Jessica Orozco 2026-05-27 12:32:57 -04:00
commit 994ed732d0
45 changed files with 9023 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
node_modules
.next
.env.local
.env.local.template
.gentle
.agents
.git
.cursor
.vscode
.claude

191
AGENT.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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.

View 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
View 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.
```

View 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.

View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
import { encrypt } from '../crypto/cipher';
export function kongEncrypt(text: string): string {
return encrypt(text);
}

View 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 });
};

View 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
View 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
View File

View 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"
}
}
}

View 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" }
]
}
}
}

View 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"
}
}
}

View 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"
}
}
}
}

View 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"
}
}
}

View 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"
}
}
}
}

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View 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
View File

@ -0,0 +1,8 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;

26
setup.ps1 Normal file
View 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
View 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
View 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"}`).

View 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.

View 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 }
]
}
}
```

View 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(', ')}`);
}
}
```

View 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 |

View 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
View 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
View 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
View 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"]
}