architecture refactoring
This commit is contained in:
13
backend/.env.example
Normal file
13
backend/.env.example
Normal file
@@ -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
|
||||||
209
backend/ARCHITECTURE.md
Normal file
209
backend/ARCHITECTURE.md
Normal file
@@ -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
|
||||||
202
backend/CHANGES_SUMMARY.md
Normal file
202
backend/CHANGES_SUMMARY.md
Normal file
@@ -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 <token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
@@ -10,6 +10,6 @@ export default defineConfig({
|
|||||||
path: "prisma/migrations",
|
path: "prisma/migrations",
|
||||||
},
|
},
|
||||||
datasource: {
|
datasource: {
|
||||||
url: env("POSTGRES_URL"),
|
url: env("DATABASE_URL"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { env } from "./config/env.js";
|
||||||
import { AppRoutes } from "./presentation/routes.js";
|
import { AppRoutes } from "./presentation/routes.js";
|
||||||
import { Server } from "./presentation/server.js";
|
import { Server } from "./presentation/server.js";
|
||||||
|
import { prisma } from "./data/postgres/index.js";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const server = new Server( {
|
try {
|
||||||
port : env.PORT,
|
// Verificar conexión a la base de datos
|
||||||
routes : AppRoutes.routes,
|
await prisma.$connect();
|
||||||
});
|
console.log("✓ Database connected");
|
||||||
await server.start();
|
|
||||||
|
|
||||||
|
// 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();
|
main();
|
||||||
18
backend/src/config/bcrypt.ts
Normal file
18
backend/src/config/bcrypt.ts
Normal file
@@ -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<string> {
|
||||||
|
return bcrypt.hash(password, env.BCRYPT_ROUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async compare(password: string, hash: string): Promise<boolean> {
|
||||||
|
return bcrypt.compare(password, hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = {
|
export const env = {
|
||||||
PORT: envVar.get('PORT').required().asPortNumber(),
|
// Server
|
||||||
PUBLIC_PATH: envVar.get('PUBLIC_PATH').default('public').asString(),
|
PORT: Number(getEnvVar("PORT", "3000")),
|
||||||
POSTGRES_URL: envVar.get('POSTGRES_URL').required().asString()
|
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;
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
export interface JwtPayload {
|
||||||
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN ?? '2h';
|
id: number;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class JwtAdapter {
|
export class JwtAdapter {
|
||||||
static generateToken(payload: object): string {
|
static generate(payload: JwtPayload): string {
|
||||||
return jwt.sign(payload, JWT_SECRET, {
|
const options: SignOptions = {
|
||||||
expiresIn: JWT_EXPIRES_IN,
|
expiresIn: env.JWT_EXPIRES_IN as NonNullable<SignOptions["expiresIn"]>,
|
||||||
} as SignOptions);
|
};
|
||||||
|
|
||||||
|
return jwt.sign(payload, env.JWT_SEED, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateToken<T>(token: string): T | null {
|
static validate(token: string): JwtPayload | null {
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, JWT_SECRET) as T;
|
return jwt.verify(token, env.JWT_SEED) as JwtPayload;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
import { PrismaClient } from "../../generated/prisma/client.js";
|
||||||
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
const connectionString = `${env.POSTGRES_URL}`;
|
|
||||||
const adapter = new PrismaPg({ connectionString });
|
const adapter = new PrismaPg({
|
||||||
|
connectionString: process.env.DATABASE_URL!,
|
||||||
export const prisma = new PrismaClient({ adapter });
|
});
|
||||||
|
|
||||||
|
export const prisma = new PrismaClient({
|
||||||
|
adapter,
|
||||||
|
errorFormat: "pretty",
|
||||||
|
});
|
||||||
45
backend/src/data/repositories/auth.repository.impl.ts
Normal file
45
backend/src/data/repositories/auth.repository.impl.ts
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,38 @@
|
|||||||
export class LoginUserDto {
|
/**
|
||||||
private constructor(
|
* login-user.dto.ts
|
||||||
public readonly email: string,
|
* DTO para iniciar sesión.
|
||||||
public readonly password: string,
|
* Define qué datos esperamos al hacer login.
|
||||||
) {}
|
*/
|
||||||
|
|
||||||
static create(props: { [key: string]: any }): [string | undefined, LoginUserDto?] {
|
export interface LoginUserDto {
|
||||||
const { email, password } = props;
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
if (!email) return ['email is required'];
|
export class LoginUserDtoValidator {
|
||||||
if (!password) return ['password is required'];
|
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<string, unknown>;
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,44 @@
|
|||||||
export class RegisterUserDto {
|
/**
|
||||||
private constructor(
|
* register-user.dto.ts
|
||||||
public readonly name: string,
|
* DTO para registrar un usuario.
|
||||||
public readonly email: string,
|
* Define qué datos esperamos al registrar.
|
||||||
public readonly password: string,
|
*/
|
||||||
) {}
|
|
||||||
|
|
||||||
static create(props: { [key: string]: any }): [string | undefined, RegisterUserDto?] {
|
export interface RegisterUserDto {
|
||||||
const { name, email, password } = props;
|
name: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
if (!name) return ['name is required'];
|
export class RegisterUserDtoValidator {
|
||||||
if (!email) return ['email is required'];
|
static validate(data: unknown): RegisterUserDto {
|
||||||
if (!password) return ['password is required'];
|
if (typeof data !== "object" || data === null) {
|
||||||
|
throw new Error("Invalid data");
|
||||||
if (password.length < 6) {
|
|
||||||
return ['password must be at least 6 characters'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [undefined, new RegisterUserDto(name, email, password)];
|
const obj = data as Record<string, unknown>;
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
35
backend/src/domain/errors/custom.error.ts
Normal file
35
backend/src/domain/errors/custom.error.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
backend/src/domain/repositories/auth.repository.ts
Normal file
11
backend/src/domain/repositories/auth.repository.ts
Normal file
@@ -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>;
|
||||||
|
}
|
||||||
30
backend/src/domain/use-cases/auth/get-me.use-case.ts
Normal file
30
backend/src/domain/use-cases/auth/get-me.use-case.ts
Normal file
@@ -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<GetMeResponse> {
|
||||||
|
const user = await this.repository.findById(userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw CustomError.notFound("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return user as GetMeResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
backend/src/domain/use-cases/auth/login-user.use-case.ts
Normal file
59
backend/src/domain/use-cases/auth/login-user.use-case.ts
Normal file
@@ -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<LoginUserResponse> {
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/src/domain/use-cases/auth/register-user.use-case.ts
Normal file
60
backend/src/domain/use-cases/auth/register-user.use-case.ts
Normal file
@@ -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<RegisterUserResponse> {
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 { Response } from "express";
|
||||||
import { JwtAdapter } from "../../config/jwt.js";
|
|
||||||
import { LoginUserDto, RegisterUserDto } from "../../domain/dtos/index.js";
|
|
||||||
import { AuthRequest } from "../middlewares/auth.middleware.js";
|
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 {
|
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
|
register = async (req: AuthRequest, res: Response) => {
|
||||||
constructor() {}
|
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) {
|
const result = await this.getMeUseCase.execute(req.user.id);
|
||||||
return res.status(400).json({ error });
|
res.status(200).json(result);
|
||||||
}
|
} catch (error) {
|
||||||
|
this.handleError(error, res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
private handleError(error: unknown, res: Response): void {
|
||||||
const userExists = await prisma.user.findUnique({
|
if (error instanceof CustomError) {
|
||||||
where: {
|
res.status(error.statusCode).json({ error: error.message });
|
||||||
email: registerUserDto!.email,
|
return;
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public loginUser = async (req: Request, res: Response) => {
|
if (error instanceof Error) {
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
const [error, loginUserDto] = LoginUserDto.create(req.body);
|
return;
|
||||||
|
|
||||||
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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMe = async (req: AuthRequest, res: Response) => {
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
if (!req.user) {
|
|
||||||
return res.status(401).json({
|
|
||||||
error: 'User not authenticated'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
user: req.user
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* routes.ts
|
||||||
|
* Rutas de autenticación.
|
||||||
|
* Define los endpoints de register, login y getMe.
|
||||||
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { AuthController } from "./controller.js";
|
import { AuthController } from "./controller.js";
|
||||||
import { AuthMiddleware } from "../middlewares/auth.middleware.js";
|
import { AuthMiddleware } from "../middlewares/auth.middleware.js";
|
||||||
|
|
||||||
export class AuthRoutes {
|
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();
|
return 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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 {
|
import { Request, Response, NextFunction } from "express";
|
||||||
id: number;
|
import { JwtAdapter, JwtPayload } from "../../config/jwt.js";
|
||||||
email: string;
|
import { AuthRepositoryImpl } from "../../data/repositories/auth.repository.impl.js";
|
||||||
role: string;
|
import { CustomError } from "../../domain/errors/custom.error.js";
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthRequest extends Request {
|
export interface AuthRequest extends Request {
|
||||||
user?: TokenPayload;
|
user?: JwtPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthMiddleware {
|
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) {
|
// Verificar que el usuario existe en BD
|
||||||
return res.status(401).json({
|
const repository = new AuthRepositoryImpl();
|
||||||
error: 'No token provided'
|
const user = await repository.findById(payload.id);
|
||||||
});
|
if (!user) {
|
||||||
}
|
throw CustomError.unauthorized("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
if (!authorization.startsWith('Bearer ')) {
|
// Agregar el usuario al request
|
||||||
return res.status(401).json({
|
req.user = payload;
|
||||||
error: 'Invalid Bearer token'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authorization.split(' ').at(1) ?? '';
|
next();
|
||||||
|
} catch (error) {
|
||||||
const payload = JwtAdapter.validateToken<TokenPayload>(token);
|
if (error instanceof CustomError) {
|
||||||
|
return res.status(error.statusCode).json({ error: error.message });
|
||||||
if (!payload) {
|
}
|
||||||
return res.status(401).json({
|
res.status(500).json({ error: "Internal server error" });
|
||||||
error: 'Invalid token'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = payload;
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user