From c2e53eb21b07013a55e364a3ddd1a03b33c2e030 Mon Sep 17 00:00:00 2001 From: Cesar Date: Fri, 22 May 2026 19:26:38 -0600 Subject: [PATCH 1/4] architecture refactoring --- backend/.env.example | 13 ++ backend/ARCHITECTURE.md | 209 ++++++++++++++++++ backend/CHANGES_SUMMARY.md | 202 +++++++++++++++++ backend/prisma.config.ts | 2 +- backend/src/app.ts | 31 ++- backend/src/config/bcrypt.ts | 18 ++ backend/src/config/env.ts | 41 +++- backend/src/config/jwt.ts | 23 +- backend/src/data/postgres/index.ts | 19 +- .../data/repositories/auth.repository.impl.ts | 45 ++++ .../src/domain/dtos/auth/login-user.dto.ts | 43 +++- .../src/domain/dtos/auth/register-user.dto.ts | 53 +++-- backend/src/domain/errors/custom.error.ts | 35 +++ .../domain/repositories/auth.repository.ts | 11 + .../domain/use-cases/auth/get-me.use-case.ts | 30 +++ .../use-cases/auth/login-user.use-case.ts | 59 +++++ .../use-cases/auth/register-user.use-case.ts | 60 +++++ backend/src/presentation/auth/controller.ts | 177 +++++---------- backend/src/presentation/auth/routes.ts | 26 ++- .../middlewares/auth.middleware.ts | 74 ++++--- 20 files changed, 949 insertions(+), 222 deletions(-) create mode 100644 backend/.env.example create mode 100644 backend/ARCHITECTURE.md create mode 100644 backend/CHANGES_SUMMARY.md create mode 100644 backend/src/config/bcrypt.ts create mode 100644 backend/src/data/repositories/auth.repository.impl.ts create mode 100644 backend/src/domain/errors/custom.error.ts create mode 100644 backend/src/domain/repositories/auth.repository.ts create mode 100644 backend/src/domain/use-cases/auth/get-me.use-case.ts create mode 100644 backend/src/domain/use-cases/auth/login-user.use-case.ts create mode 100644 backend/src/domain/use-cases/auth/register-user.use-case.ts diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..c743b02 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,13 @@ +# Server +PORT=3000 +NODE_ENV=development + +# Database +DATABASE_URL=postgresql://user:password@localhost:5432/optihack + +# JWT +JWT_SEED=your_super_secret_jwt_seed_here_change_in_production +JWT_EXPIRES_IN=7d + +# Bcrypt +BCRYPT_ROUNDS=10 diff --git a/backend/ARCHITECTURE.md b/backend/ARCHITECTURE.md new file mode 100644 index 0000000..ec54cdc --- /dev/null +++ b/backend/ARCHITECTURE.md @@ -0,0 +1,209 @@ +# Clean Architecture - Backend OptiHack + +## Estructura del Proyecto + +``` +src/ +├── config/ # Configuración e integración con librerías +│ ├── env.ts # Variables de entorno +│ ├── jwt.ts # JwtAdapter - genera y valida tokens +│ └── bcrypt.ts # BcryptAdapter - hashea y compara contraseñas +│ +├── domain/ # Lógica de negocio (sin dependencias externas) +│ ├── dtos/ # Data Transfer Objects con validación +│ │ ├── auth/ +│ │ │ ├── register-user.dto.ts +│ │ │ └── login-user.dto.ts +│ │ └── index.ts +│ │ +│ ├── repositories/ # Interfaces de repositorios (contratos) +│ │ └── auth.repository.ts +│ │ +│ ├── use-cases/ # Lógica de negocio (casos de uso) +│ │ └── auth/ +│ │ ├── register-user.use-case.ts +│ │ ├── login-user.use-case.ts +│ │ └── get-me.use-case.ts +│ │ +│ └── errors/ # Errores personalizados +│ └── custom.error.ts +│ +├── data/ # Implementación de datos (Prisma, BD) +│ ├── postgres/ # Cliente de Prisma +│ │ └── index.ts +│ └── repositories/ # Implementación de repositorios +│ └── auth.repository.impl.ts +│ +├── presentation/ # HTTP (Express, controladores, middlewares) +│ ├── auth/ +│ │ ├── controller.ts +│ │ └── routes.ts +│ ├── middlewares/ +│ │ └── auth.middleware.ts +│ ├── routes.ts # Rutas principales +│ └── server.ts # Instancia de Express +│ +├── app.ts # Punto de entrada +└── ... +``` + +## Flujo de Datos (Regla Principal) + +``` +HTTP Request + ↓ +Controller recibe los datos + ↓ +Controller llama al UseCase + ↓ +UseCase hace la lógica de negocio + ↓ +UseCase llama al Repository + ↓ +Repository implementación habla con Prisma + ↓ +Prisma ejecuta queries en la BD + ↓ +Response va hacia arriba + ↓ +HTTP Response +``` + +### Ejemplo: Register + +1. **Cliente** envía POST `/api/auth/register` con `{ name, email, password }` +2. **Controller** recibe la request +3. **Controller** llama a `RegisterUserUseCase.execute()` +4. **UseCase** valida el DTO +5. **UseCase** verifica si el email existe (llama al Repository) +6. **Repository** busca en la BD +7. **UseCase** hashea la contraseña +8. **UseCase** crea el usuario (llama al Repository) +9. **Repository** inserta en la BD +10. **UseCase** genera el JWT +11. **Controller** responde con user + token + +## Responsabilidades por Capa + +### **Config** +- Variables de entorno +- Adapters (JWT, Bcrypt) +- Configuraciones globales + +### **Domain** (Sin dependencias externas) +- DTOs con validación +- Interfaces de repositorios (contratos) +- Casos de uso (lógica de negocio) +- Errores personalizados + +### **Data** +- Implementación de repositorios +- Cliente de Prisma +- Queries a la base de datos + +### **Presentation** +- Controladores (Express handlers) +- Rutas +- Middlewares +- Validación HTTP + +## Endpoints de Autenticación + +### 1. Register +``` +POST /api/auth/register +Content-Type: application/json + +{ + "name": "John Doe", + "email": "john@example.com", + "password": "password123" +} + +Response 201: +{ + "user": { + "id": 1, + "email": "john@example.com", + "name": "John Doe" + }, + "token": "eyJhbGc..." +} +``` + +### 2. Login +``` +POST /api/auth/login +Content-Type: application/json + +{ + "email": "john@example.com", + "password": "password123" +} + +Response 200: +{ + "user": { + "id": 1, + "email": "john@example.com", + "name": "John Doe" + }, + "token": "eyJhbGc..." +} +``` + +### 3. GetMe (Protegida) +``` +GET /api/auth/me +Authorization: Bearer eyJhbGc... + +Response 200: +{ + "id": 1, + "email": "john@example.com", + "name": "John Doe", + "role": "USER" +} +``` + +## Validaciones + +### DTOs +- **name**: string no vacío +- **email**: email válido (regex) +- **password**: mínimo 6 caracteres + +### Errores +- **400 Bad Request**: Datos inválidos +- **401 Unauthorized**: Token inválido, usuario no autenticado +- **404 Not Found**: Usuario no encontrado +- **409 Conflict**: Email ya en uso +- **500 Internal Server Error**: Error del servidor + +## Variables de Entorno (.env) + +``` +PORT=3000 # Puerto del servidor +NODE_ENV=development # Entorno +DATABASE_URL=postgresql://user:pass@host:5432/db # URL de BD +JWT_SEED=tu_secreto_super_seguro # Seed para JWT +JWT_EXPIRES_IN=7d # Expiración del token +BCRYPT_ROUNDS=10 # Rondas de bcrypt +``` + +## Cómo Agregar un Nuevo UseCase + +1. **Crear el UseCase** en `domain/use-cases/auth/new-feature.use-case.ts` +2. **Agregar método al Repository** en `domain/repositories/auth.repository.ts` +3. **Implementar en** `data/repositories/auth.repository.impl.ts` +4. **Crear método en Controller** en `presentation/auth/controller.ts` +5. **Agregar ruta** en `presentation/auth/routes.ts` + +## Ventajas de Esta Arquitectura + +✓ **Separación de responsabilidades**: Cada capa hace una cosa +✓ **Fácil de testear**: UseCase no conoce HTTP ni BD +✓ **Mantenible**: Cambios en BD no afectan lógica de negocio +✓ **Escalable**: Fácil agregar nuevos módulos +✓ **Simple**: No over-engineered, ideal para MVP/Hackathon +✓ **Limpio**: Código organizado y fácil de entender diff --git a/backend/CHANGES_SUMMARY.md b/backend/CHANGES_SUMMARY.md new file mode 100644 index 0000000..c588441 --- /dev/null +++ b/backend/CHANGES_SUMMARY.md @@ -0,0 +1,202 @@ +# Resumen de Archivos - Clean Architecture + +## Archivos Modificados/Creados + +### 1. **config/env.ts** ✓ +- Lee y valida variables de entorno +- Centraliza PORT, DATABASE_URL, JWT_SEED, BCRYPT_ROUNDS +- Se importa en toda la app para acceder a configuración + +### 2. **config/jwt.ts** ✓ +- JwtAdapter para generar y validar tokens +- Métodos estáticos: `generate()` y `validate()` +- Define JwtPayload interface con id y email +- Exporta los tokens y valida automáticamente expiración + +### 3. **config/bcrypt.ts** ✓ +- BcryptAdapter para hashear y comparar contraseñas +- Métodos estáticos: `hash()` y `compare()` +- Usa BCRYPT_ROUNDS de env para consistencia + +### 4. **domain/errors/custom.error.ts** ✓ +- CustomError clase base para errores personalizados +- Métodos estáticos: badRequest(), unauthorized(), notFound(), conflict(), internalServer() +- Cada error tiene statusCode para responder correctamente + +### 5. **domain/dtos/auth/register-user.dto.ts** ✓ +- RegisterUserDto interfaz +- RegisterUserDtoValidator con método validate() +- Valida name (no vacío), email (formato), password (mínimo 6 caracteres) +- Normaliza email a minúsculas + +### 6. **domain/dtos/auth/login-user.dto.ts** ✓ +- LoginUserDto interfaz +- LoginUserDtoValidator con método validate() +- Valida email (formato) y password (no vacío) +- Normaliza email a minúsculas + +### 7. **domain/dtos/index.ts** ✓ +- Exporta todos los DTOs en un solo lugar +- Facilita importar: `import * from '../../dtos'` + +### 8. **domain/repositories/auth.repository.ts** ✓ +- AuthRepository interfaz (contrato) +- Define métodos: findByEmail(), create(), findById() +- El UseCase no sabe cómo se implementa, solo el contrato + +### 9. **domain/use-cases/auth/register-user.use-case.ts** ✓ +- RegisterUserUseCase ejecuta el registro +- Valida DTO → Verifica email → Hashea → Crea usuario → Genera JWT +- Retorna { user, token } +- Lanza CustomError si email existe + +### 10. **domain/use-cases/auth/login-user.use-case.ts** ✓ +- LoginUserUseCase ejecuta el login +- Valida DTO → Busca usuario → Compara password → Genera JWT +- Retorna { user, token } +- Lanza CustomError si credenciales son inválidas + +### 11. **domain/use-cases/auth/get-me.use-case.ts** ✓ +- GetMeUseCase obtiene el usuario autenticado +- Recibe userId del JWT +- Busca en repositorio y retorna datos del usuario +- Lanza CustomError si no existe + +### 12. **data/postgres/index.ts** ✓ +- Cliente único de Prisma +- Se inicializa una vez y se reutiliza +- Se exporta para usarlo en repositorios + +### 13. **data/repositories/auth.repository.impl.ts** ✓ +- AuthRepositoryImpl implementa AuthRepository +- Habla directamente con Prisma (BD) +- Métodos: findByEmail(), create(), findById() +- Select solo los campos necesarios (no contraseña en getMe) + +### 14. **presentation/middlewares/auth.middleware.ts** ✓ +- AuthMiddleware valida JWT en rutas protegidas +- Extrae token del header: "Authorization: Bearer TOKEN" +- Valida token → Busca usuario en BD → Agrega a req.user +- Define AuthRequest extends Request con user opcional +- Retorna 401 si token inválido o usuario no existe + +### 15. **presentation/auth/controller.ts** ✓ +- AuthController maneja peticiones HTTP +- Métodos: register(), login(), getMe() +- Cada método recibe, llama al UseCase, responde +- handleError() maneja CustomError y error normal + +### 16. **presentation/auth/routes.ts** ✓ +- Define rutas de autenticación +- POST /api/auth/register → controller.register +- POST /api/auth/login → controller.login +- GET /api/auth/me → AuthMiddleware.validate → controller.getMe + +### 17. **presentation/routes.ts** ✓ +- Rutas principales de la app +- Integra AuthRoutes en /api/auth + +### 18. **presentation/server.ts** (Sin cambios) +- Instancia Express +- Middlewares globales (json, urlencoded) + +### 19. **app.ts** ✓ +- Punto de entrada +- Conecta a Prisma +- Crea Server con env.PORT +- Maneja errores globales + +### 20. **.env.example** ✓ +- Ejemplo de variables de entorno +- El usuario debe copiar a .env y configurar valores + +### 21. **ARCHITECTURE.md** ✓ +- Documentación completa de la arquitectura +- Estructura de carpetas +- Flujo de datos +- Endpoints +- Variables de entorno + +--- + +## Resumen de Cambios + +### Lógica de Negocio (Domain) +- DTOs con validaciones robustas +- UseCase implementan la lógica de registrar, login, obtener usuario +- Repository interface define el contrato +- CustomError para manejo de errores consistente + +### Implementación (Data) +- AuthRepositoryImpl usa Prisma para hablar con BD +- Select solo campos necesarios (seguridad) +- Reutiliza cliente de Prisma + +### HTTP (Presentation) +- AuthController coordina peticiones HTTP +- AuthMiddleware valida JWT en rutas protegidas +- AuthRoutes define endpoints + +### Configuración (Config) +- JwtAdapter y BcryptAdapter centralizados +- env.ts maneja todas las variables + +--- + +## Cómo Usar + +### 1. Copiar .env.example a .env +```bash +cp .env.example .env +``` + +### 2. Configurar variables de entorno +```bash +# .env +DATABASE_URL=postgresql://user:password@localhost:5432/optihack +JWT_SEED=mi_secreto_super_seguro +``` + +### 3. Instalar dependencias +```bash +npm install +``` + +### 4. Ejecutar migraciones de Prisma +```bash +npx prisma migrate dev +``` + +### 5. Iniciar el servidor +```bash +npm run dev +``` + +### 6. Probar endpoints +```bash +# Register +curl -X POST http://localhost:3000/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{"name":"John","email":"john@example.com","password":"password123"}' + +# Login +curl -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"john@example.com","password":"password123"}' + +# GetMe (usa el token obtenido en login) +curl -X GET http://localhost:3000/api/auth/me \ + -H "Authorization: Bearer " +``` + +--- + +## Principios Aplicados + +✓ **Separation of Concerns**: Cada capa tiene una responsabilidad +✓ **Dependency Inversion**: UseCase depende de interfaz, no implementación +✓ **Single Responsibility**: Cada archivo hace una cosa bien +✓ **Open/Closed**: Fácil agregar nuevos casos de uso +✓ **Liskov Substitution**: AuthRepositoryImpl puede reemplazar AuthRepository +✓ **Interface Segregation**: Interfaces pequeñas y específicas +✓ **DRY**: No repitas código, centraliza lógica diff --git a/backend/prisma.config.ts b/backend/prisma.config.ts index 3c45216..60d6fe9 100644 --- a/backend/prisma.config.ts +++ b/backend/prisma.config.ts @@ -10,6 +10,6 @@ export default defineConfig({ path: "prisma/migrations", }, datasource: { - url: env("POSTGRES_URL"), + url: env("DATABASE_URL"), }, }); diff --git a/backend/src/app.ts b/backend/src/app.ts index 071cf34..40fd2ec 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,15 +1,34 @@ +/** + * app.ts + * Punto de entrada de la aplicación. + * - Inicia el servidor + * - Conecta a la base de datos + * - Maneja errores globales + */ + +import "dotenv/config"; import { env } from "./config/env.js"; import { AppRoutes } from "./presentation/routes.js"; import { Server } from "./presentation/server.js"; - +import { prisma } from "./data/postgres/index.js"; async function main() { - const server = new Server( { - port : env.PORT, - routes : AppRoutes.routes, -}); - await server.start(); + try { + // Verificar conexión a la base de datos + await prisma.$connect(); + console.log("✓ Database connected"); + // Crear y iniciar el servidor + const server = new Server({ + port: env.PORT, + routes: AppRoutes.routes, + }); + + await server.start(); + } catch (error) { + console.error("✗ Error starting application:", error); + process.exit(1); + } } main(); \ No newline at end of file diff --git a/backend/src/config/bcrypt.ts b/backend/src/config/bcrypt.ts new file mode 100644 index 0000000..bcc6e62 --- /dev/null +++ b/backend/src/config/bcrypt.ts @@ -0,0 +1,18 @@ +/** + * bcrypt.ts + * BcryptAdapter: hashea y compara contraseñas. + * Encapsula toda la lógica de bcrypt en un lugar. + */ + +import bcrypt from "bcryptjs"; +import { env } from "./env.js"; + +export class BcryptAdapter { + static async hash(password: string): Promise { + return bcrypt.hash(password, env.BCRYPT_ROUNDS); + } + + static async compare(password: string, hash: string): Promise { + return bcrypt.compare(password, hash); + } +} diff --git a/backend/src/config/env.ts b/backend/src/config/env.ts index a631686..b599db4 100644 --- a/backend/src/config/env.ts +++ b/backend/src/config/env.ts @@ -1,9 +1,38 @@ -import 'dotenv/config'; -import envVar from 'env-var'; +/** + * env.ts + * Lee y valida las variables de entorno. + * Centraliza toda la configuración que viene del .env + */ + +const getEnvVar = (key: string, defaultValue?: string): string => { + const value = process.env[key]; + if (!value && defaultValue === undefined) { + throw new Error(`Missing environment variable: ${key}`); + } + return (value as string) || (defaultValue as string); +}; + +const requiredEnvVar = (key: string): string => { + const value = process.env[key]; + if (!value) { + throw new Error(`Missing required environment variable: ${key}`); + } + return value; +}; export const env = { - PORT: envVar.get('PORT').required().asPortNumber(), - PUBLIC_PATH: envVar.get('PUBLIC_PATH').default('public').asString(), - POSTGRES_URL: envVar.get('POSTGRES_URL').required().asString() -} + // Server + PORT: Number(getEnvVar("PORT", "3000")), + NODE_ENV: getEnvVar("NODE_ENV", "development"), + + // Database + DATABASE_URL: requiredEnvVar("DATABASE_URL"), + + // JWT + JWT_SEED: requiredEnvVar("JWT_SEED"), + JWT_EXPIRES_IN: getEnvVar("JWT_EXPIRES_IN", "7d"), + + // Bcrypt + BCRYPT_ROUNDS: Number(getEnvVar("BCRYPT_ROUNDS", "10")), +} as const; diff --git a/backend/src/config/jwt.ts b/backend/src/config/jwt.ts index 1b1ff37..73ec18e 100644 --- a/backend/src/config/jwt.ts +++ b/backend/src/config/jwt.ts @@ -1,18 +1,23 @@ -import jwt, { SignOptions } from 'jsonwebtoken'; +import jwt, { SignOptions } from "jsonwebtoken"; +import { env } from "./env.js"; -const JWT_SECRET = process.env.JWT_SECRET ?? 'dev_secret'; -const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN ?? '2h'; +export interface JwtPayload { + id: number; + email: string; +} export class JwtAdapter { - static generateToken(payload: object): string { - return jwt.sign(payload, JWT_SECRET, { - expiresIn: JWT_EXPIRES_IN, - } as SignOptions); + static generate(payload: JwtPayload): string { + const options: SignOptions = { + expiresIn: env.JWT_EXPIRES_IN as NonNullable, + }; + + return jwt.sign(payload, env.JWT_SEED, options); } - static validateToken(token: string): T | null { + static validate(token: string): JwtPayload | null { try { - return jwt.verify(token, JWT_SECRET) as T; + return jwt.verify(token, env.JWT_SEED) as JwtPayload; } catch { return null; } diff --git a/backend/src/data/postgres/index.ts b/backend/src/data/postgres/index.ts index 5d90ca3..a14c73d 100644 --- a/backend/src/data/postgres/index.ts +++ b/backend/src/data/postgres/index.ts @@ -1,9 +1,14 @@ -import { env } from "../../config/env.js"; -import { PrismaPg } from "@prisma/adapter-pg"; + +import "dotenv/config"; import { PrismaClient } from "../../generated/prisma/client.js"; - -const connectionString = `${env.POSTGRES_URL}`; -const adapter = new PrismaPg({ connectionString }); - -export const prisma = new PrismaClient({ adapter }); \ No newline at end of file +import { PrismaPg } from "@prisma/adapter-pg"; + +const adapter = new PrismaPg({ + connectionString: process.env.DATABASE_URL!, +}); + +export const prisma = new PrismaClient({ + adapter, + errorFormat: "pretty", +}); \ No newline at end of file diff --git a/backend/src/data/repositories/auth.repository.impl.ts b/backend/src/data/repositories/auth.repository.impl.ts new file mode 100644 index 0000000..3fec4d7 --- /dev/null +++ b/backend/src/data/repositories/auth.repository.impl.ts @@ -0,0 +1,45 @@ +/** + * auth.repository.impl.ts + * AuthRepositoryImpl: implementación real del AuthRepository. + * Aquí es donde hablamos con la base de datos usando Prisma. + */ + +import { AuthRepository } from "../../domain/repositories/auth.repository.js"; +import { prisma } from "../postgres/index.js"; + +export class AuthRepositoryImpl implements AuthRepository { + async findByEmail(email: string) { + return prisma.user.findUnique({ + where: { email }, + select: { + id: true, + email: true, + password: true, + name: true, + }, + }); + } + + async create(data: { name: string; email: string; password: string }) { + return prisma.user.create({ + data, + select: { + id: true, + email: true, + name: true, + }, + }); + } + + async findById(id: number) { + return prisma.user.findUnique({ + where: { id }, + select: { + id: true, + email: true, + name: true, + role: true, + }, + }); + } +} \ No newline at end of file diff --git a/backend/src/domain/dtos/auth/login-user.dto.ts b/backend/src/domain/dtos/auth/login-user.dto.ts index aec887b..9965c85 100644 --- a/backend/src/domain/dtos/auth/login-user.dto.ts +++ b/backend/src/domain/dtos/auth/login-user.dto.ts @@ -1,15 +1,38 @@ -export class LoginUserDto { - private constructor( - public readonly email: string, - public readonly password: string, - ) {} +/** + * login-user.dto.ts + * DTO para iniciar sesión. + * Define qué datos esperamos al hacer login. + */ - static create(props: { [key: string]: any }): [string | undefined, LoginUserDto?] { - const { email, password } = props; +export interface LoginUserDto { + email: string; + password: string; +} - if (!email) return ['email is required']; - if (!password) return ['password is required']; +export class LoginUserDtoValidator { + static validate(data: unknown): LoginUserDto { + if (typeof data !== "object" || data === null) { + throw new Error("Invalid data"); + } - return [undefined, new LoginUserDto(email, password)]; + const obj = data as Record; + + if (typeof obj.email !== "string" || !this.isValidEmail(obj.email)) { + throw new Error("Email is required and must be valid"); + } + + if (typeof obj.password !== "string" || obj.password.length === 0) { + throw new Error("Password is required"); + } + + return { + email: obj.email.trim().toLowerCase(), + password: obj.password, + }; + } + + private static isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); } } \ No newline at end of file diff --git a/backend/src/domain/dtos/auth/register-user.dto.ts b/backend/src/domain/dtos/auth/register-user.dto.ts index 59f623a..3266f8b 100644 --- a/backend/src/domain/dtos/auth/register-user.dto.ts +++ b/backend/src/domain/dtos/auth/register-user.dto.ts @@ -1,21 +1,44 @@ -export class RegisterUserDto { - private constructor( - public readonly name: string, - public readonly email: string, - public readonly password: string, - ) {} +/** + * register-user.dto.ts + * DTO para registrar un usuario. + * Define qué datos esperamos al registrar. + */ - static create(props: { [key: string]: any }): [string | undefined, RegisterUserDto?] { - const { name, email, password } = props; +export interface RegisterUserDto { + name: string; + email: string; + password: string; +} - if (!name) return ['name is required']; - if (!email) return ['email is required']; - if (!password) return ['password is required']; - - if (password.length < 6) { - return ['password must be at least 6 characters']; +export class RegisterUserDtoValidator { + static validate(data: unknown): RegisterUserDto { + if (typeof data !== "object" || data === null) { + throw new Error("Invalid data"); } - return [undefined, new RegisterUserDto(name, email, password)]; + const obj = data as Record; + + if (typeof obj.name !== "string" || obj.name.trim().length === 0) { + throw new Error("Name is required and must be a non-empty string"); + } + + if (typeof obj.email !== "string" || !this.isValidEmail(obj.email)) { + throw new Error("Email is required and must be valid"); + } + + if (typeof obj.password !== "string" || obj.password.length < 6) { + throw new Error("Password is required and must be at least 6 characters"); + } + + return { + name: obj.name.trim(), + email: obj.email.trim().toLowerCase(), + password: obj.password, + }; + } + + private static isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); } } \ No newline at end of file diff --git a/backend/src/domain/errors/custom.error.ts b/backend/src/domain/errors/custom.error.ts new file mode 100644 index 0000000..4315215 --- /dev/null +++ b/backend/src/domain/errors/custom.error.ts @@ -0,0 +1,35 @@ +/** + * custom.error.ts + * CustomError: clase base para errores personalizados. + * Facilita el manejo de errores en toda la aplicación. + */ + +export class CustomError extends Error { + constructor( + public message: string, + public statusCode: number, + ) { + super(message); + this.name = "CustomError"; + } + + static badRequest(message: string): CustomError { + return new CustomError(message, 400); + } + + static unauthorized(message: string = "Unauthorized"): CustomError { + return new CustomError(message, 401); + } + + static notFound(message: string): CustomError { + return new CustomError(message, 404); + } + + static conflict(message: string): CustomError { + return new CustomError(message, 409); + } + + static internalServer(message: string = "Internal Server Error"): CustomError { + return new CustomError(message, 500); + } +} diff --git a/backend/src/domain/repositories/auth.repository.ts b/backend/src/domain/repositories/auth.repository.ts new file mode 100644 index 0000000..b89b37d --- /dev/null +++ b/backend/src/domain/repositories/auth.repository.ts @@ -0,0 +1,11 @@ +/** + * auth.repository.ts + * AuthRepository (interfaz): define qué métodos debe tener el repositorio de auth. + * El use-case usa esta interfaz sin saber cómo se implementa. + */ + +export interface AuthRepository { + findByEmail(email: string): Promise<{ id: number; email: string; password: string; name: string } | null>; + create(data: { name: string; email: string; password: string }): Promise<{ id: number; email: string; name: string }>; + findById(id: number): Promise<{ id: number; email: string; name: string; role: string } | null>; +} \ No newline at end of file diff --git a/backend/src/domain/use-cases/auth/get-me.use-case.ts b/backend/src/domain/use-cases/auth/get-me.use-case.ts new file mode 100644 index 0000000..dbc73d0 --- /dev/null +++ b/backend/src/domain/use-cases/auth/get-me.use-case.ts @@ -0,0 +1,30 @@ +/** + * get-me.use-case.ts + * GetMeUseCase: obtiene los datos del usuario autenticado. + * - Busca el usuario por ID (que viene del JWT) + * - Retorna los datos del usuario + */ + +import { AuthRepository } from "../../repositories/auth.repository.js"; +import { CustomError } from "../../errors/custom.error.js"; + +export interface GetMeResponse { + id: number; + email: string; + name: string; + role: string; +} + +export class GetMeUseCase { + constructor(private repository: AuthRepository) {} + + async execute(userId: number): Promise { + const user = await this.repository.findById(userId); + + if (!user) { + throw CustomError.notFound("User not found"); + } + + return user as GetMeResponse; + } +} \ No newline at end of file diff --git a/backend/src/domain/use-cases/auth/login-user.use-case.ts b/backend/src/domain/use-cases/auth/login-user.use-case.ts new file mode 100644 index 0000000..1cd838c --- /dev/null +++ b/backend/src/domain/use-cases/auth/login-user.use-case.ts @@ -0,0 +1,59 @@ +/** + * login-user.use-case.ts + * LoginUserUseCase: lógica para iniciar sesión. + * - Valida datos + * - Busca el usuario por email + * - Compara las contraseñas + * - Genera el JWT + */ + +import { LoginUserDto, LoginUserDtoValidator } from "../../dtos/index.js"; +import { AuthRepository } from "../../repositories/auth.repository.js"; +import { CustomError } from "../../errors/custom.error.js"; +import { BcryptAdapter } from "../../../config/bcrypt.js"; +import { JwtAdapter } from "../../../config/jwt.js"; + +export interface LoginUserResponse { + user: { + id: number; + email: string; + name: string; + }; + token: string; +} + +export class LoginUserUseCase { + constructor(private repository: AuthRepository) {} + + async execute(data: unknown): Promise { + // Validar DTO + const dto = LoginUserDtoValidator.validate(data); + + // Buscar usuario por email + const user = await this.repository.findByEmail(dto.email); + if (!user) { + throw CustomError.unauthorized("Invalid credentials"); + } + + // Comparar contraseñas + const isPasswordValid = await BcryptAdapter.compare(dto.password, user.password); + if (!isPasswordValid) { + throw CustomError.unauthorized("Invalid credentials"); + } + + // Generar JWT + const token = JwtAdapter.generate({ + id: user.id, + email: user.email, + }); + + return { + user: { + id: user.id, + email: user.email, + name: user.name, + }, + token, + }; + } +} \ No newline at end of file diff --git a/backend/src/domain/use-cases/auth/register-user.use-case.ts b/backend/src/domain/use-cases/auth/register-user.use-case.ts new file mode 100644 index 0000000..cdd649c --- /dev/null +++ b/backend/src/domain/use-cases/auth/register-user.use-case.ts @@ -0,0 +1,60 @@ +/** + * register-user.use-case.ts + * RegisterUserUseCase: lógica para registrar un usuario. + * - Valida datos + * - Verifica si el email ya existe + * - Hashea la contraseña + * - Crea el usuario + * - Genera el JWT + */ + +import { RegisterUserDto, RegisterUserDtoValidator } from "../../dtos/index.js"; +import { AuthRepository } from "../../repositories/auth.repository.js"; +import { CustomError } from "../../errors/custom.error.js"; +import { BcryptAdapter } from "../../../config/bcrypt.js"; +import { JwtAdapter } from "../../../config/jwt.js"; + +export interface RegisterUserResponse { + user: { + id: number; + email: string; + name: string; + }; + token: string; +} + +export class RegisterUserUseCase { + constructor(private repository: AuthRepository) {} + + async execute(data: unknown): Promise { + // Validar DTO + const dto = RegisterUserDtoValidator.validate(data); + + // Verificar si el email ya existe + const existingUser = await this.repository.findByEmail(dto.email); + if (existingUser) { + throw CustomError.conflict("Email already in use"); + } + + // Hashear la contraseña + const hashedPassword = await BcryptAdapter.hash(dto.password); + + // Crear el usuario + const user = await this.repository.create({ + name: dto.name, + email: dto.email, + password: hashedPassword, + }); + + // Generar JWT + const token = JwtAdapter.generate({ + id: user.id, + email: user.email, + }); + + return { + user, + token, + }; + } +} diff --git a/backend/src/presentation/auth/controller.ts b/backend/src/presentation/auth/controller.ts index b9c7b2b..3f9c941 100644 --- a/backend/src/presentation/auth/controller.ts +++ b/backend/src/presentation/auth/controller.ts @@ -1,137 +1,68 @@ -import { Request, Response } from "express"; -import bcrypt from "bcryptjs"; +/** + * controller.ts + * AuthController: maneja las peticiones HTTP de autenticación. + * - Recibe los datos + * - Llama al use-case correspondiente + * - Responde al cliente + * - Maneja los errores + */ -import { prisma } from "../../data/postgres/index.js"; -import { JwtAdapter } from "../../config/jwt.js"; -import { LoginUserDto, RegisterUserDto } from "../../domain/dtos/index.js"; +import { Response } from "express"; import { AuthRequest } from "../middlewares/auth.middleware.js"; +import { CustomError } from "../../domain/errors/custom.error.js"; +import { AuthRepositoryImpl } from "../../data/repositories/auth.repository.impl.js"; +import { RegisterUserUseCase } from "../../domain/use-cases/auth/register-user.use-case.js"; +import { LoginUserUseCase } from "../../domain/use-cases/auth/login-user.use-case.js"; +import { GetMeUseCase } from "../../domain/use-cases/auth/get-me.use-case.js"; export class AuthController { + private repository = new AuthRepositoryImpl(); + private registerUseCase = new RegisterUserUseCase(this.repository); + private loginUseCase = new LoginUserUseCase(this.repository); + private getMeUseCase = new GetMeUseCase(this.repository); - // SIN metodos estaticos para hacer DI - constructor() {} + register = async (req: AuthRequest, res: Response) => { + try { + const result = await this.registerUseCase.execute(req.body); + res.status(201).json(result); + } catch (error) { + this.handleError(error, res); + } + }; - public registerUser = async (req: Request, res: Response) => { + login = async (req: AuthRequest, res: Response) => { + try { + const result = await this.loginUseCase.execute(req.body); + res.status(200).json(result); + } catch (error) { + this.handleError(error, res); + } + }; - const [error, registerUserDto] = RegisterUserDto.create(req.body); + getMe = async (req: AuthRequest, res: Response) => { + try { + if (!req.user) { + throw CustomError.unauthorized("User not authenticated"); + } - if (error) { - return res.status(400).json({ error }); - } + const result = await this.getMeUseCase.execute(req.user.id); + res.status(200).json(result); + } catch (error) { + this.handleError(error, res); + } + }; - try { - const userExists = await prisma.user.findUnique({ - where: { - email: registerUserDto!.email, - } - }); - - if (userExists) { - return res.status(400).json({ - error: 'Email already exists' - }); - } - - const hashedPassword = bcrypt.hashSync(registerUserDto!.password, 10); - - const user = await prisma.user.create({ - data: { - name: registerUserDto!.name, - email: registerUserDto!.email, - password: hashedPassword, - } - }); - - const token = JwtAdapter.generateToken({ - id: user.id, - email: user.email, - role: user.role, - }); - - return res.status(201).json({ - user: { - id: user.id, - name: user.name, - email: user.email, - role: user.role, - }, - token - }); - - } catch (error) { - console.log(error); - return res.status(500).json({ - error: 'Internal server error' - }); - } + private handleError(error: unknown, res: Response): void { + if (error instanceof CustomError) { + res.status(error.statusCode).json({ error: error.message }); + return; } - public loginUser = async (req: Request, res: Response) => { - - const [error, loginUserDto] = LoginUserDto.create(req.body); - - if (error) { - return res.status(400).json({ error }); - } - - try { - const user = await prisma.user.findUnique({ - where: { - email: loginUserDto!.email, - } - }); - - if (!user) { - return res.status(400).json({ - error: 'Invalid email or password' - }); - } - - const isPasswordValid = bcrypt.compareSync( - loginUserDto!.password, - user.password - ); - - if (!isPasswordValid) { - return res.status(400).json({ - error: 'Invalid email or password' - }); - } - - const token = JwtAdapter.generateToken({ - id: user.id, - email: user.email, - role: user.role, - }); - - return res.json({ - user: { - id: user.id, - name: user.name, - email: user.email, - role: user.role, - }, - token - }); - - } catch (error) { - console.log(error); - return res.status(500).json({ - error: 'Internal server error' - }); - } + if (error instanceof Error) { + res.status(400).json({ error: error.message }); + return; } - public getMe = async (req: AuthRequest, res: Response) => { - - if (!req.user) { - return res.status(401).json({ - error: 'User not authenticated' - }); - } - - return res.json({ - user: req.user - }); - } + res.status(500).json({ error: "Internal server error" }); + } } \ No newline at end of file diff --git a/backend/src/presentation/auth/routes.ts b/backend/src/presentation/auth/routes.ts index 2913c41..e8808d4 100644 --- a/backend/src/presentation/auth/routes.ts +++ b/backend/src/presentation/auth/routes.ts @@ -1,20 +1,22 @@ +/** + * routes.ts + * Rutas de autenticación. + * Define los endpoints de register, login y getMe. + */ + import { Router } from "express"; import { AuthController } from "./controller.js"; import { AuthMiddleware } from "../middlewares/auth.middleware.js"; export class AuthRoutes { + static get routes(): Router { + const router = Router(); + const controller = new AuthController(); - static get routes(): Router { + router.post("/register", controller.register); + router.post("/login", controller.login); + router.get("/me", AuthMiddleware.validate, controller.getMe); - const router = Router(); - const controller = new AuthController(); - - router.post('/register', controller.registerUser); - router.post('/login', controller.loginUser); - - // Ruta protegida para probar JWT - router.get('/me', AuthMiddleware.validateJwt, controller.getMe); - - return router; - } + return router; + } } \ No newline at end of file diff --git a/backend/src/presentation/middlewares/auth.middleware.ts b/backend/src/presentation/middlewares/auth.middleware.ts index c1daaa4..a605845 100644 --- a/backend/src/presentation/middlewares/auth.middleware.ts +++ b/backend/src/presentation/middlewares/auth.middleware.ts @@ -1,46 +1,54 @@ -import { Request, Response, NextFunction } from "express"; -import { JwtAdapter } from "../../config/jwt.js"; +/** + * auth.middleware.ts + * Middleware para validar el JWT. + * - Lee el token del header Authorization: Bearer TOKEN + * - Valida el token + * - Busca el usuario en base de datos + * - Agrega el usuario a req.user + */ -interface TokenPayload { - id: number; - email: string; - role: string; -} +import { Request, Response, NextFunction } from "express"; +import { JwtAdapter, JwtPayload } from "../../config/jwt.js"; +import { AuthRepositoryImpl } from "../../data/repositories/auth.repository.impl.js"; +import { CustomError } from "../../domain/errors/custom.error.js"; export interface AuthRequest extends Request { - user?: TokenPayload; + user?: JwtPayload; } export class AuthMiddleware { + static async validate(req: AuthRequest, res: Response, next: NextFunction) { + try { + // Obtener el token del header + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + throw CustomError.unauthorized("Missing or invalid Authorization header"); + } - static validateJwt = (req: AuthRequest, res: Response, next: NextFunction) => { + const token = authHeader.substring(7); // Remover "Bearer " - const authorization = req.header('Authorization'); + // Validar el token + const payload = JwtAdapter.validate(token); + if (!payload) { + throw CustomError.unauthorized("Invalid token"); + } - if (!authorization) { - return res.status(401).json({ - error: 'No token provided' - }); - } + // Verificar que el usuario existe en BD + const repository = new AuthRepositoryImpl(); + const user = await repository.findById(payload.id); + if (!user) { + throw CustomError.unauthorized("User not found"); + } - if (!authorization.startsWith('Bearer ')) { - return res.status(401).json({ - error: 'Invalid Bearer token' - }); - } + // Agregar el usuario al request + req.user = payload; - const token = authorization.split(' ').at(1) ?? ''; - - const payload = JwtAdapter.validateToken(token); - - if (!payload) { - return res.status(401).json({ - error: 'Invalid token' - }); - } - - req.user = payload; - - next(); + next(); + } catch (error) { + if (error instanceof CustomError) { + return res.status(error.statusCode).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); } + } } \ No newline at end of file From 2bb271712b93723cce8eee4018318ac32ba2ded4 Mon Sep 17 00:00:00 2001 From: imsophis Date: Fri, 22 May 2026 19:46:23 -0600 Subject: [PATCH 2/4] feat: setup bottom tab navigation and base screens --- frontend/src/app/_layout.tsx | 73 ++++++++++++++++++++++++++++++------ frontend/src/app/guide.tsx | 2 +- frontend/src/app/index.tsx | 2 +- frontend/src/app/profile.tsx | 14 +++---- 4 files changed, 70 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/_layout.tsx b/frontend/src/app/_layout.tsx index 7a02f47..f354518 100644 --- a/frontend/src/app/_layout.tsx +++ b/frontend/src/app/_layout.tsx @@ -11,12 +11,45 @@ export default function Layout() { tabBarInactiveTintColor: "#9CA3AF", tabBarStyle: { - height: 70, - paddingBottom: 10, - paddingTop: 10, + position: "absolute", + bottom: 5, + left: 20, + right: 20, + + height: 82, + + borderRadius: 25, + backgroundColor: "#FFFFFF", + borderTopWidth: 0, - elevation: 8, + + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 10, + }, + + shadowOpacity: 0.08, + shadowRadius: 10, + + elevation: 10, + + paddingHorizontal: 10, + }, + + tabBarLabelStyle: { + fontSize: 11, + fontWeight: "400", + marginBottom: 8, + }, + + tabBarItemStyle: { + paddingTop: 6, + paddingBottom: 4, + borderRadius: 18, + marginVertical: 6, + marginHorizontal: 4, }, }} > @@ -24,8 +57,12 @@ export default function Layout() { name="index" options={{ title: "Inicio", - tabBarIcon: ({ color, size }) => ( - + tabBarIcon: ({ focused, color }) => ( + ), }} /> @@ -34,8 +71,12 @@ export default function Layout() { name="alerts" options={{ title: "Alertas", - tabBarIcon: ({ color, size }) => ( - + tabBarIcon: ({ focused, color }) => ( + ), }} /> @@ -44,8 +85,12 @@ export default function Layout() { name="guide" options={{ title: "Guía", - tabBarIcon: ({ color, size }) => ( - + tabBarIcon: ({ focused, color }) => ( + ), }} /> @@ -54,8 +99,12 @@ export default function Layout() { name="profile" options={{ title: "Perfil", - tabBarIcon: ({ color, size }) => ( - + tabBarIcon: ({ focused, color }) => ( + ), }} /> diff --git a/frontend/src/app/guide.tsx b/frontend/src/app/guide.tsx index fbdcea9..3561942 100644 --- a/frontend/src/app/guide.tsx +++ b/frontend/src/app/guide.tsx @@ -8,7 +8,7 @@ export default function GuideScreen() { justifyContent: "center", alignItems: "center", }} - > + > Guía ); diff --git a/frontend/src/app/index.tsx b/frontend/src/app/index.tsx index 648f45d..752dd59 100644 --- a/frontend/src/app/index.tsx +++ b/frontend/src/app/index.tsx @@ -12,4 +12,4 @@ export default function HomeScreen() { Inicio ); -} +} \ No newline at end of file diff --git a/frontend/src/app/profile.tsx b/frontend/src/app/profile.tsx index 23dee45..754f25d 100644 --- a/frontend/src/app/profile.tsx +++ b/frontend/src/app/profile.tsx @@ -4,12 +4,12 @@ export default function ProfileScreen() { return ( - Perfil + flex: 1, + justifyContent: "center", + alignItems: "center", + }} + > + Perfil ); -} \ No newline at end of file +} From 32c7c7b31b742f64ea685c84a10902eb0e0f581b Mon Sep 17 00:00:00 2001 From: hack_25030344_757942 <25030344@itcelaya.edu.mx> Date: Sat, 23 May 2026 01:50:09 +0000 Subject: [PATCH 3/4] Delete backend/ARCHITECTURE.md --- backend/ARCHITECTURE.md | 209 ---------------------------------------- 1 file changed, 209 deletions(-) delete mode 100644 backend/ARCHITECTURE.md diff --git a/backend/ARCHITECTURE.md b/backend/ARCHITECTURE.md deleted file mode 100644 index ec54cdc..0000000 --- a/backend/ARCHITECTURE.md +++ /dev/null @@ -1,209 +0,0 @@ -# Clean Architecture - Backend OptiHack - -## Estructura del Proyecto - -``` -src/ -├── config/ # Configuración e integración con librerías -│ ├── env.ts # Variables de entorno -│ ├── jwt.ts # JwtAdapter - genera y valida tokens -│ └── bcrypt.ts # BcryptAdapter - hashea y compara contraseñas -│ -├── domain/ # Lógica de negocio (sin dependencias externas) -│ ├── dtos/ # Data Transfer Objects con validación -│ │ ├── auth/ -│ │ │ ├── register-user.dto.ts -│ │ │ └── login-user.dto.ts -│ │ └── index.ts -│ │ -│ ├── repositories/ # Interfaces de repositorios (contratos) -│ │ └── auth.repository.ts -│ │ -│ ├── use-cases/ # Lógica de negocio (casos de uso) -│ │ └── auth/ -│ │ ├── register-user.use-case.ts -│ │ ├── login-user.use-case.ts -│ │ └── get-me.use-case.ts -│ │ -│ └── errors/ # Errores personalizados -│ └── custom.error.ts -│ -├── data/ # Implementación de datos (Prisma, BD) -│ ├── postgres/ # Cliente de Prisma -│ │ └── index.ts -│ └── repositories/ # Implementación de repositorios -│ └── auth.repository.impl.ts -│ -├── presentation/ # HTTP (Express, controladores, middlewares) -│ ├── auth/ -│ │ ├── controller.ts -│ │ └── routes.ts -│ ├── middlewares/ -│ │ └── auth.middleware.ts -│ ├── routes.ts # Rutas principales -│ └── server.ts # Instancia de Express -│ -├── app.ts # Punto de entrada -└── ... -``` - -## Flujo de Datos (Regla Principal) - -``` -HTTP Request - ↓ -Controller recibe los datos - ↓ -Controller llama al UseCase - ↓ -UseCase hace la lógica de negocio - ↓ -UseCase llama al Repository - ↓ -Repository implementación habla con Prisma - ↓ -Prisma ejecuta queries en la BD - ↓ -Response va hacia arriba - ↓ -HTTP Response -``` - -### Ejemplo: Register - -1. **Cliente** envía POST `/api/auth/register` con `{ name, email, password }` -2. **Controller** recibe la request -3. **Controller** llama a `RegisterUserUseCase.execute()` -4. **UseCase** valida el DTO -5. **UseCase** verifica si el email existe (llama al Repository) -6. **Repository** busca en la BD -7. **UseCase** hashea la contraseña -8. **UseCase** crea el usuario (llama al Repository) -9. **Repository** inserta en la BD -10. **UseCase** genera el JWT -11. **Controller** responde con user + token - -## Responsabilidades por Capa - -### **Config** -- Variables de entorno -- Adapters (JWT, Bcrypt) -- Configuraciones globales - -### **Domain** (Sin dependencias externas) -- DTOs con validación -- Interfaces de repositorios (contratos) -- Casos de uso (lógica de negocio) -- Errores personalizados - -### **Data** -- Implementación de repositorios -- Cliente de Prisma -- Queries a la base de datos - -### **Presentation** -- Controladores (Express handlers) -- Rutas -- Middlewares -- Validación HTTP - -## Endpoints de Autenticación - -### 1. Register -``` -POST /api/auth/register -Content-Type: application/json - -{ - "name": "John Doe", - "email": "john@example.com", - "password": "password123" -} - -Response 201: -{ - "user": { - "id": 1, - "email": "john@example.com", - "name": "John Doe" - }, - "token": "eyJhbGc..." -} -``` - -### 2. Login -``` -POST /api/auth/login -Content-Type: application/json - -{ - "email": "john@example.com", - "password": "password123" -} - -Response 200: -{ - "user": { - "id": 1, - "email": "john@example.com", - "name": "John Doe" - }, - "token": "eyJhbGc..." -} -``` - -### 3. GetMe (Protegida) -``` -GET /api/auth/me -Authorization: Bearer eyJhbGc... - -Response 200: -{ - "id": 1, - "email": "john@example.com", - "name": "John Doe", - "role": "USER" -} -``` - -## Validaciones - -### DTOs -- **name**: string no vacío -- **email**: email válido (regex) -- **password**: mínimo 6 caracteres - -### Errores -- **400 Bad Request**: Datos inválidos -- **401 Unauthorized**: Token inválido, usuario no autenticado -- **404 Not Found**: Usuario no encontrado -- **409 Conflict**: Email ya en uso -- **500 Internal Server Error**: Error del servidor - -## Variables de Entorno (.env) - -``` -PORT=3000 # Puerto del servidor -NODE_ENV=development # Entorno -DATABASE_URL=postgresql://user:pass@host:5432/db # URL de BD -JWT_SEED=tu_secreto_super_seguro # Seed para JWT -JWT_EXPIRES_IN=7d # Expiración del token -BCRYPT_ROUNDS=10 # Rondas de bcrypt -``` - -## Cómo Agregar un Nuevo UseCase - -1. **Crear el UseCase** en `domain/use-cases/auth/new-feature.use-case.ts` -2. **Agregar método al Repository** en `domain/repositories/auth.repository.ts` -3. **Implementar en** `data/repositories/auth.repository.impl.ts` -4. **Crear método en Controller** en `presentation/auth/controller.ts` -5. **Agregar ruta** en `presentation/auth/routes.ts` - -## Ventajas de Esta Arquitectura - -✓ **Separación de responsabilidades**: Cada capa hace una cosa -✓ **Fácil de testear**: UseCase no conoce HTTP ni BD -✓ **Mantenible**: Cambios en BD no afectan lógica de negocio -✓ **Escalable**: Fácil agregar nuevos módulos -✓ **Simple**: No over-engineered, ideal para MVP/Hackathon -✓ **Limpio**: Código organizado y fácil de entender From 4f6ec41b3215ccab36f63fc907f5a43491d94fc8 Mon Sep 17 00:00:00 2001 From: hack_25030344_757942 <25030344@itcelaya.edu.mx> Date: Sat, 23 May 2026 01:50:14 +0000 Subject: [PATCH 4/4] Delete backend/CHANGES_SUMMARY.md --- backend/CHANGES_SUMMARY.md | 202 ------------------------------------- 1 file changed, 202 deletions(-) delete mode 100644 backend/CHANGES_SUMMARY.md diff --git a/backend/CHANGES_SUMMARY.md b/backend/CHANGES_SUMMARY.md deleted file mode 100644 index c588441..0000000 --- a/backend/CHANGES_SUMMARY.md +++ /dev/null @@ -1,202 +0,0 @@ -# Resumen de Archivos - Clean Architecture - -## Archivos Modificados/Creados - -### 1. **config/env.ts** ✓ -- Lee y valida variables de entorno -- Centraliza PORT, DATABASE_URL, JWT_SEED, BCRYPT_ROUNDS -- Se importa en toda la app para acceder a configuración - -### 2. **config/jwt.ts** ✓ -- JwtAdapter para generar y validar tokens -- Métodos estáticos: `generate()` y `validate()` -- Define JwtPayload interface con id y email -- Exporta los tokens y valida automáticamente expiración - -### 3. **config/bcrypt.ts** ✓ -- BcryptAdapter para hashear y comparar contraseñas -- Métodos estáticos: `hash()` y `compare()` -- Usa BCRYPT_ROUNDS de env para consistencia - -### 4. **domain/errors/custom.error.ts** ✓ -- CustomError clase base para errores personalizados -- Métodos estáticos: badRequest(), unauthorized(), notFound(), conflict(), internalServer() -- Cada error tiene statusCode para responder correctamente - -### 5. **domain/dtos/auth/register-user.dto.ts** ✓ -- RegisterUserDto interfaz -- RegisterUserDtoValidator con método validate() -- Valida name (no vacío), email (formato), password (mínimo 6 caracteres) -- Normaliza email a minúsculas - -### 6. **domain/dtos/auth/login-user.dto.ts** ✓ -- LoginUserDto interfaz -- LoginUserDtoValidator con método validate() -- Valida email (formato) y password (no vacío) -- Normaliza email a minúsculas - -### 7. **domain/dtos/index.ts** ✓ -- Exporta todos los DTOs en un solo lugar -- Facilita importar: `import * from '../../dtos'` - -### 8. **domain/repositories/auth.repository.ts** ✓ -- AuthRepository interfaz (contrato) -- Define métodos: findByEmail(), create(), findById() -- El UseCase no sabe cómo se implementa, solo el contrato - -### 9. **domain/use-cases/auth/register-user.use-case.ts** ✓ -- RegisterUserUseCase ejecuta el registro -- Valida DTO → Verifica email → Hashea → Crea usuario → Genera JWT -- Retorna { user, token } -- Lanza CustomError si email existe - -### 10. **domain/use-cases/auth/login-user.use-case.ts** ✓ -- LoginUserUseCase ejecuta el login -- Valida DTO → Busca usuario → Compara password → Genera JWT -- Retorna { user, token } -- Lanza CustomError si credenciales son inválidas - -### 11. **domain/use-cases/auth/get-me.use-case.ts** ✓ -- GetMeUseCase obtiene el usuario autenticado -- Recibe userId del JWT -- Busca en repositorio y retorna datos del usuario -- Lanza CustomError si no existe - -### 12. **data/postgres/index.ts** ✓ -- Cliente único de Prisma -- Se inicializa una vez y se reutiliza -- Se exporta para usarlo en repositorios - -### 13. **data/repositories/auth.repository.impl.ts** ✓ -- AuthRepositoryImpl implementa AuthRepository -- Habla directamente con Prisma (BD) -- Métodos: findByEmail(), create(), findById() -- Select solo los campos necesarios (no contraseña en getMe) - -### 14. **presentation/middlewares/auth.middleware.ts** ✓ -- AuthMiddleware valida JWT en rutas protegidas -- Extrae token del header: "Authorization: Bearer TOKEN" -- Valida token → Busca usuario en BD → Agrega a req.user -- Define AuthRequest extends Request con user opcional -- Retorna 401 si token inválido o usuario no existe - -### 15. **presentation/auth/controller.ts** ✓ -- AuthController maneja peticiones HTTP -- Métodos: register(), login(), getMe() -- Cada método recibe, llama al UseCase, responde -- handleError() maneja CustomError y error normal - -### 16. **presentation/auth/routes.ts** ✓ -- Define rutas de autenticación -- POST /api/auth/register → controller.register -- POST /api/auth/login → controller.login -- GET /api/auth/me → AuthMiddleware.validate → controller.getMe - -### 17. **presentation/routes.ts** ✓ -- Rutas principales de la app -- Integra AuthRoutes en /api/auth - -### 18. **presentation/server.ts** (Sin cambios) -- Instancia Express -- Middlewares globales (json, urlencoded) - -### 19. **app.ts** ✓ -- Punto de entrada -- Conecta a Prisma -- Crea Server con env.PORT -- Maneja errores globales - -### 20. **.env.example** ✓ -- Ejemplo de variables de entorno -- El usuario debe copiar a .env y configurar valores - -### 21. **ARCHITECTURE.md** ✓ -- Documentación completa de la arquitectura -- Estructura de carpetas -- Flujo de datos -- Endpoints -- Variables de entorno - ---- - -## Resumen de Cambios - -### Lógica de Negocio (Domain) -- DTOs con validaciones robustas -- UseCase implementan la lógica de registrar, login, obtener usuario -- Repository interface define el contrato -- CustomError para manejo de errores consistente - -### Implementación (Data) -- AuthRepositoryImpl usa Prisma para hablar con BD -- Select solo campos necesarios (seguridad) -- Reutiliza cliente de Prisma - -### HTTP (Presentation) -- AuthController coordina peticiones HTTP -- AuthMiddleware valida JWT en rutas protegidas -- AuthRoutes define endpoints - -### Configuración (Config) -- JwtAdapter y BcryptAdapter centralizados -- env.ts maneja todas las variables - ---- - -## Cómo Usar - -### 1. Copiar .env.example a .env -```bash -cp .env.example .env -``` - -### 2. Configurar variables de entorno -```bash -# .env -DATABASE_URL=postgresql://user:password@localhost:5432/optihack -JWT_SEED=mi_secreto_super_seguro -``` - -### 3. Instalar dependencias -```bash -npm install -``` - -### 4. Ejecutar migraciones de Prisma -```bash -npx prisma migrate dev -``` - -### 5. Iniciar el servidor -```bash -npm run dev -``` - -### 6. Probar endpoints -```bash -# Register -curl -X POST http://localhost:3000/api/auth/register \ - -H "Content-Type: application/json" \ - -d '{"name":"John","email":"john@example.com","password":"password123"}' - -# Login -curl -X POST http://localhost:3000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"email":"john@example.com","password":"password123"}' - -# GetMe (usa el token obtenido en login) -curl -X GET http://localhost:3000/api/auth/me \ - -H "Authorization: Bearer " -``` - ---- - -## Principios Aplicados - -✓ **Separation of Concerns**: Cada capa tiene una responsabilidad -✓ **Dependency Inversion**: UseCase depende de interfaz, no implementación -✓ **Single Responsibility**: Cada archivo hace una cosa bien -✓ **Open/Closed**: Fácil agregar nuevos casos de uso -✓ **Liskov Substitution**: AuthRepositoryImpl puede reemplazar AuthRepository -✓ **Interface Segregation**: Interfaces pequeñas y específicas -✓ **DRY**: No repitas código, centraliza lógica