feat: add JWT authentication
This commit is contained in:
@@ -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();
|
||||
9
backend/src/config/env.ts
Normal file
9
backend/src/config/env.ts
Normal 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
20
backend/src/config/jwt.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
backend/src/data/postgres/index.ts
Normal file
9
backend/src/data/postgres/index.ts
Normal 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 });
|
||||
15
backend/src/domain/dtos/auth/login-user.dto.ts
Normal file
15
backend/src/domain/dtos/auth/login-user.dto.ts
Normal 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)];
|
||||
}
|
||||
}
|
||||
21
backend/src/domain/dtos/auth/register-user.dto.ts
Normal file
21
backend/src/domain/dtos/auth/register-user.dto.ts
Normal 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)];
|
||||
}
|
||||
}
|
||||
2
backend/src/domain/dtos/index.ts
Normal file
2
backend/src/domain/dtos/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './auth/register-user.dto.js';
|
||||
export * from './auth/login-user.dto.js';
|
||||
137
backend/src/presentation/auth/controller.ts
Normal file
137
backend/src/presentation/auth/controller.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
20
backend/src/presentation/auth/routes.ts
Normal file
20
backend/src/presentation/auth/routes.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
46
backend/src/presentation/middlewares/auth.middleware.ts
Normal file
46
backend/src/presentation/middlewares/auth.middleware.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
14
backend/src/presentation/routes.ts
Normal file
14
backend/src/presentation/routes.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
46
backend/src/presentation/server.ts
Normal file
46
backend/src/presentation/server.ts
Normal 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}`);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user