diff --git a/package-lock.json b/package-lock.json index 51ece84..1180aad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b1078c0..e86e9b8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/persona/jwt.guard.ts b/src/persona/jwt.guard.ts new file mode 100644 index 0000000..1ca9664 --- /dev/null +++ b/src/persona/jwt.guard.ts @@ -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; + } +} diff --git a/src/persona/jwt.strategy.ts b/src/persona/jwt.strategy.ts new file mode 100644 index 0000000..8f9800b --- /dev/null +++ b/src/persona/jwt.strategy.ts @@ -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('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 }; + } +} diff --git a/src/persona/persona.controller.ts b/src/persona/persona.controller.ts index 4edb9fd..46278f8 100644 --- a/src/persona/persona.controller.ts +++ b/src/persona/persona.controller.ts @@ -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, }, - }); + }; } } diff --git a/src/persona/persona.module.ts b/src/persona/persona.module.ts index c934a50..c9ae287 100644 --- a/src/persona/persona.module.ts +++ b/src/persona/persona.module.ts @@ -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('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('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('JWT_SECRET'), + signOptions: { expiresIn: '1h' }, }; }, }), ], - controllers: [PersonaController], + controllers: [PersonaController, JwtStrategy], providers: [PersonaService, GoogleStrategy], }) export class PersonaModule {} diff --git a/src/persona/persona.service.ts b/src/persona/persona.service.ts index a1d3fbd..5ef92e8 100644 --- a/src/persona/persona.service.ts +++ b/src/persona/persona.service.ts @@ -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 { + 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; + } }