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/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 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 4ef3629..752dd59 100644 --- a/frontend/src/app/index.tsx +++ b/frontend/src/app/index.tsx @@ -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() { Inicio ); -}*/ - -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 ( - - - {/*ETA*/} - - - {/*ACCIONES RÁPIDAS*/} - - - - - - - - - {/*IMPACTO SEMANAL*/} - - - - - - - - - - - {/* 🔔 ALERTAS RECIENTES */} - - - - - - - - - - - - ); } \ 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 +}