app
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
|
||||
@@ -10,6 +10,6 @@ export default defineConfig({
|
||||
path: "prisma/migrations",
|
||||
},
|
||||
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 { 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();
|
||||
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 = {
|
||||
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;
|
||||
|
||||
|
||||
@@ -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<SignOptions["expiresIn"]>,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, env.JWT_SEED, options);
|
||||
}
|
||||
|
||||
static validateToken<T>(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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
|
||||
const adapter = new PrismaPg({
|
||||
connectionString: process.env.DATABASE_URL!,
|
||||
});
|
||||
|
||||
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(
|
||||
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<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(
|
||||
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<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 { 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" });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<TokenPayload>(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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }) => (
|
||||
<Ionicons name="home-outline" size={size} color={color} />
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Ionicons
|
||||
name={focused ? "home" : "home-outline"}
|
||||
size={focused ? 28 : 24}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@@ -34,8 +71,12 @@ export default function Layout() {
|
||||
name="alerts"
|
||||
options={{
|
||||
title: "Alertas",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="notifications-outline" size={size} color={color} />
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Ionicons
|
||||
name={focused ? "notifications" : "notifications-outline"}
|
||||
size={focused ? 28 : 24}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@@ -44,8 +85,12 @@ export default function Layout() {
|
||||
name="guide"
|
||||
options={{
|
||||
title: "Guía",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="leaf-outline" size={size} color={color} />
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Ionicons
|
||||
name={focused ? "leaf" : "leaf-outline"}
|
||||
size={focused ? 28 : 24}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@@ -54,8 +99,12 @@ export default function Layout() {
|
||||
name="profile"
|
||||
options={{
|
||||
title: "Perfil",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="person-outline" size={size} color={color} />
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Ionicons
|
||||
name={focused ? "person" : "person-outline"}
|
||||
size={focused ? 28 : 24}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function GuideScreen() {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
>
|
||||
<Text>Guía</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*import { View, Text, ScrollView } from "react-native";
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
export default function HomeScreen() {
|
||||
return (
|
||||
@@ -12,91 +12,4 @@ export default function HomeScreen() {
|
||||
<Text>Inicio</Text>
|
||||
</View>
|
||||
);
|
||||
}*/
|
||||
|
||||
import { View, ScrollView } from 'react-native';
|
||||
|
||||
import EtaCard from '../components/EtaCard';
|
||||
import QuickAction from '../components/QuickAction';
|
||||
import SectionTitle from '../components/SectionTitle';
|
||||
import AlertItem from '../components/Alertltem';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<ScrollView
|
||||
style={{ flex: 1, backgroundColor: '#ffffff' }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
|
||||
{/*ETA*/}
|
||||
<EtaCard
|
||||
minutes={12}
|
||||
status="Camión en ruta 🚛"
|
||||
/>
|
||||
|
||||
{/*ACCIONES RÁPIDAS*/}
|
||||
<SectionTitle title="Acciones rápidas" />
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 10,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
<QuickAction title="Mapa" icon="🗺️" />
|
||||
<QuickAction title="Reportar" icon="🚨" />
|
||||
<QuickAction title="Horarios" icon="🕒" />
|
||||
</View>
|
||||
|
||||
{/*IMPACTO SEMANAL*/}
|
||||
<SectionTitle title="Impacto semanal" />
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginHorizontal: 15,
|
||||
backgroundColor: '#F0F0F3',
|
||||
padding: 20,
|
||||
borderRadius: 16,
|
||||
}}
|
||||
>
|
||||
<AlertItem
|
||||
message="🚛 5 recolecciones completadas"
|
||||
type="success"
|
||||
/>
|
||||
|
||||
<AlertItem
|
||||
message="⏱ Sin retrasos esta semana"
|
||||
type="success"
|
||||
/>
|
||||
|
||||
<AlertItem
|
||||
message="📍 Cobertura completa en tu zona"
|
||||
type="success"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 🔔 ALERTAS RECIENTES */}
|
||||
<SectionTitle title="Alertas recientes" />
|
||||
|
||||
<View style={{ marginBottom: 40 }}>
|
||||
<AlertItem
|
||||
message="🚛 Ruta iniciada"
|
||||
type="success"
|
||||
/>
|
||||
|
||||
<AlertItem
|
||||
message="📍 Camión cerca de tu zona"
|
||||
type="warning"
|
||||
/>
|
||||
|
||||
<AlertItem
|
||||
message="⏳ Retraso detectado"
|
||||
type="danger"
|
||||
/>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -4,12 +4,12 @@ export default function ProfileScreen() {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text>Perfil</Text>
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text>Perfil</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user