add jwt guard and another EP to return information

This commit is contained in:
2025-10-17 17:17:44 -06:00
parent 3acb35437d
commit 95913a8ecc
7 changed files with 118 additions and 31 deletions
+32
View File
@@ -18,9 +18,11 @@
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^6.0.0",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.15.2",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.27"
@@ -37,6 +39,7 @@
"@types/node": "^22.10.7",
"@types/passport": "^1.0.17",
"@types/passport-google-oauth20": "^2.0.16",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
@@ -2857,6 +2860,16 @@
"@types/passport-oauth2": "*"
}
},
"node_modules/@types/passport-jwt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
"integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==",
"dev": true,
"dependencies": {
"@types/jsonwebtoken": "*",
"@types/passport-strategy": "*"
}
},
"node_modules/@types/passport-oauth2": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
@@ -2868,6 +2881,16 @@
"@types/passport": "*"
}
},
"node_modules/@types/passport-strategy": {
"version": "0.2.38",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
"integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*"
}
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
@@ -7973,6 +7996,15 @@
"node": ">= 0.4.0"
}
},
"node_modules/passport-jwt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
"dependencies": {
"jsonwebtoken": "^9.0.0",
"passport-strategy": "^1.0.0"
}
},
"node_modules/passport-oauth2": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
+3
View File
@@ -29,9 +29,11 @@
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^6.0.0",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.15.2",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.27"
@@ -48,6 +50,7 @@
"@types/node": "^22.10.7",
"@types/passport": "^1.0.17",
"@types/passport-google-oauth20": "^2.0.16",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
+16
View File
@@ -0,0 +1,16 @@
import {
ExecutionContext,
Injectable,
ForbiddenException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err: any, user: any, info: any, context: ExecutionContext) {
if (err || !user) {
throw new ForbiddenException('Acceso denegado: token inválido o ausente');
}
return user;
}
}
+27
View File
@@ -0,0 +1,27 @@
import { Injectable, ForbiddenException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
export interface JwtPayload {
id: string;
usuario: string;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET')!,
});
}
async validate(payload: JwtPayload) {
if (!payload || !payload.id || !payload.usuario) {
throw new ForbiddenException('Token inválido o corrupto');
}
return { id: payload.id, usuario: payload.usuario };
}
}
+17 -8
View File
@@ -2,6 +2,7 @@ import { Controller, Get, UseGuards, Req, Res } from '@nestjs/common';
import type { Response } from 'express';
import { PersonaService } from './persona.service';
import { GoogleAuthGuard } from './google-auth.guard';
import { JwtAuthGuard } from './jwt.guard';
@Controller('persona')
export class PersonaController {
@@ -12,14 +13,10 @@ export class PersonaController {
return this.personaService.findAll();
}
// Redirige a Google
@Get('google')
@UseGuards(GoogleAuthGuard)
async googleLogin() {
// NestJS + Passport se encarga de redirigir a Google
}
async googleLogin() {}
// Callback después de autenticación en Google
@Get('google/callback')
@UseGuards(GoogleAuthGuard)
async googleCallback(@Req() req, @Res() res: Response) {
@@ -32,11 +29,23 @@ export class PersonaController {
// Generar JWT con los datos de la persona
const jwt = await this.personaService.generateJwt(persona);
return res.status(200).json({
return res.redirect(
`${process.env.FRONTEND_URL}/oauth-callback?token=${jwt.access_token}`,
);
}
@UseGuards(JwtAuthGuard)
@Get('me')
async getMe(@Req() req) {
const idPersona = req.user.idPersona;
const persona = await this.personaService.findById(idPersona);
return {
error: false,
msj: `Bienvenido ${persona.nombre} ${persona.apellidoP} ${persona.apellidoM}`,
data: {
token: jwt.access_token,
token: req.headers.authorization.split(' ')[1], // token que envió el cliente
userId: persona.idPersona,
nombre: persona.nombre,
apellidoP: persona.apellidoP,
@@ -48,6 +57,6 @@ export class PersonaController {
primerLogin: persona.primerLogin,
cambioPassword: persona.cambioPasswordReq,
},
});
};
}
}
+8 -20
View File
@@ -1,43 +1,31 @@
import { Module } from '@nestjs/common';
import { PersonaService } from './persona.service';
import { PersonaController } from './persona.controller';
import { GoogleStrategy } from './google.strategy';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Persona } from './entities/persona.entity';
import { JwtModule } from '@nestjs/jwt';
import { GoogleStrategy } from './google.strategy';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
TypeOrmModule.forFeature([Persona]),
PassportModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cs: ConfigService) => {
const secret = cs.get<string>('JWT_SECRET');
if (!secret) {
throw new Error('JWT_SECRET no está definido');
}
// Se obtiene el valor de JWT_EXPIRES_IN en segundos
const expiresInStr = cs.get<string>('JWT_EXPIRES_IN') || '3600';
const expiresIn = parseInt(expiresInStr, 10); // convertir a número
if (isNaN(expiresIn)) {
throw new Error(
'JWT_EXPIRES_IN debe ser un número válido en segundos',
);
}
useFactory: async (configService: ConfigService) => {
return {
secret,
signOptions: { expiresIn }, // aquí ya es un número
global: true,
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
};
},
}),
],
controllers: [PersonaController],
controllers: [PersonaController, JwtStrategy],
providers: [PersonaService, GoogleStrategy],
})
export class PersonaModule {}
+15 -3
View File
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
@@ -31,6 +31,15 @@ export class PersonaService {
return this.personaRepository.save(persona);
}
async findById(idPersona): Promise<Persona> {
const user = await this.personaRepository.findOne({ where: { idPersona } });
if (!user) {
throw new NotFoundException('user not found');
}
return user;
}
async generateJwt(user: Persona) {
const payload = {
userType: 'usuario',
@@ -42,7 +51,6 @@ export class PersonaService {
};
}
// Opción: validateOrCreateGoogleUser para usar en GoogleStrategy
async validateOrCreateGoogleUser(data: { email: string; apellido: string }) {
let persona = await this.findByEmail(data.email);
@@ -52,7 +60,6 @@ export class PersonaService {
const apellidoM = apellidoArray[1] || '';
const nombre = apellidoArray.slice(2).join(' ') || '';
// Número identificador del correo
const numeroIdentificar = data.email.split('@')[0];
const password = await passwordRand(12);
@@ -79,4 +86,9 @@ export class PersonaService {
return persona;
}
saldo(idPersona) {
this.personaRepository.findOne({ where: { idPersona } });
return;
}
}