feat: add JWT authentication

This commit is contained in:
Cesar
2026-05-22 17:13:24 -06:00
parent 397c2ef3df
commit 45d6347d4c
15 changed files with 524 additions and 12 deletions

View File

@@ -1,16 +1,15 @@
import express from 'express';
import { env } from "./config/env.js";
import { AppRoutes } from "./presentation/routes.js";
import { Server } from "./presentation/server.js";
const app = express();
const port = process.env.PORT ?? 3000;
app.use(express.json());
app.get('/', (_req, res) => {
res.json({
message: 'API funcionando con Express + TypeScript + TSX',
});
async function main() {
const server = new Server( {
port : env.PORT,
routes : AppRoutes.routes,
});
await server.start();
app.listen(port, () => {
console.log(`Servidor corriendo en http://localhost:${port}`);
});
}
main();

View File

@@ -0,0 +1,9 @@
import 'dotenv/config';
import envVar from 'env-var';
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()
}

20
backend/src/config/jwt.ts Normal file
View File

@@ -0,0 +1,20 @@
import jwt, { SignOptions } from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET ?? 'dev_secret';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN ?? '2h';
export class JwtAdapter {
static generateToken(payload: object): string {
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
} as SignOptions);
}
static validateToken<T>(token: string): T | null {
try {
return jwt.verify(token, JWT_SECRET) as T;
} catch {
return null;
}
}
}

View File

@@ -0,0 +1,9 @@
import { env } from "../../config/env.js";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../../generated/prisma/client.js";
const connectionString = `${env.POSTGRES_URL}`;
const adapter = new PrismaPg({ connectionString });
export const prisma = new PrismaClient({ adapter });

View File

@@ -0,0 +1,15 @@
export class LoginUserDto {
private constructor(
public readonly email: string,
public readonly password: string,
) {}
static create(props: { [key: string]: any }): [string | undefined, LoginUserDto?] {
const { email, password } = props;
if (!email) return ['email is required'];
if (!password) return ['password is required'];
return [undefined, new LoginUserDto(email, password)];
}
}

View File

@@ -0,0 +1,21 @@
export class RegisterUserDto {
private constructor(
public readonly name: string,
public readonly email: string,
public readonly password: string,
) {}
static create(props: { [key: string]: any }): [string | undefined, RegisterUserDto?] {
const { name, email, password } = props;
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'];
}
return [undefined, new RegisterUserDto(name, email, password)];
}
}

View File

@@ -0,0 +1,2 @@
export * from './auth/register-user.dto.js';
export * from './auth/login-user.dto.js';

View File

@@ -0,0 +1,137 @@
import { Request, Response } from "express";
import bcrypt from "bcryptjs";
import { prisma } from "../../data/postgres/index.js";
import { JwtAdapter } from "../../config/jwt.js";
import { LoginUserDto, RegisterUserDto } from "../../domain/dtos/index.js";
import { AuthRequest } from "../middlewares/auth.middleware.js";
export class AuthController {
// SIN metodos estaticos para hacer DI
constructor() {}
public registerUser = async (req: Request, res: Response) => {
const [error, registerUserDto] = RegisterUserDto.create(req.body);
if (error) {
return res.status(400).json({ error });
}
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'
});
}
}
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'
});
}
}
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
});
}
}

View File

@@ -0,0 +1,20 @@
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();
router.post('/register', controller.registerUser);
router.post('/login', controller.loginUser);
// Ruta protegida para probar JWT
router.get('/me', AuthMiddleware.validateJwt, controller.getMe);
return router;
}
}

View File

@@ -0,0 +1,46 @@
import { Request, Response, NextFunction } from "express";
import { JwtAdapter } from "../../config/jwt.js";
interface TokenPayload {
id: number;
email: string;
role: string;
}
export interface AuthRequest extends Request {
user?: TokenPayload;
}
export class AuthMiddleware {
static validateJwt = (req: AuthRequest, res: Response, next: NextFunction) => {
const authorization = req.header('Authorization');
if (!authorization) {
return res.status(401).json({
error: 'No token provided'
});
}
if (!authorization.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Invalid Bearer token'
});
}
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();
}
}

View File

@@ -0,0 +1,14 @@
import { Router } from "express";
import { AuthRoutes } from "./auth/routes.js";
export class AppRoutes {
static get routes(): Router {
const router = Router();
router.use('/api/auth', AuthRoutes.routes);
return router;
}
}

View File

@@ -0,0 +1,46 @@
import express, { Router } from 'express';
interface Options {
port: number;
routes: Router;
}
export class Server {
private app = express();
private readonly port: number;
private readonly routes: Router;
constructor (options : Options) {
const {port, routes} = options;
this.port = port;
this.routes = routes;
}
async start() {
/*middleware*/
// para recebir el body como json
this.app.use(express.json());
// para recebir el body como urlencoded
this.app.use(express.urlencoded({ extended: true }));
//* Public folder
// Routes
this.app.use(this.routes);
// ponner servido a escuchar en el puerto 8080
this.app.listen(this.port, () => {
console.log(`Server is running on port ${this.port}`);
});
}
}