added conection to db and login with google
This commit is contained in:
Generated
+911
-81
File diff suppressed because it is too large
Load Diff
+12
-1
@@ -21,10 +21,19 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.1",
|
||||||
|
"@nestjs/mapped-types": "*",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"mysql2": "^3.15.2",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1",
|
||||||
|
"typeorm": "^0.3.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
@@ -35,6 +44,8 @@
|
|||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/passport": "^1.0.17",
|
||||||
|
"@types/passport-google-oauth20": "^2.0.16",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
|||||||
+25
-1
@@ -1,9 +1,33 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { PersonaModule } from './persona/persona.module';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: async (config: ConfigService) => ({
|
||||||
|
type: 'mysql', // ejemplo, ajusta a tu BD
|
||||||
|
host: config.get<string>('DB_HOST'),
|
||||||
|
port: config.get<number>('DB_PORT'),
|
||||||
|
username: config.get<string>('DB_USER'),
|
||||||
|
password: config.get<string>('DB_PASS'),
|
||||||
|
database: config.get<string>('DB_NAME'),
|
||||||
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
|
synchronize: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
PersonaModule,
|
||||||
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export class CreatePersonaDto {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreatePersonaDto } from './create-persona.dto';
|
||||||
|
|
||||||
|
export class UpdatePersonaDto extends PartialType(CreatePersonaDto) {}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ name: 'persona' })
|
||||||
|
export class Persona {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
idPersona: number;
|
||||||
|
|
||||||
|
@Column({ length: 50 })
|
||||||
|
nombre: string;
|
||||||
|
|
||||||
|
@Column({ length: 50 })
|
||||||
|
apellidoP: string;
|
||||||
|
|
||||||
|
@Column({ length: 50 })
|
||||||
|
apellidoM: string;
|
||||||
|
|
||||||
|
@Column({ length: 150 })
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@Column({ length: 150, nullable: true })
|
||||||
|
passwordTem?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'datetime', nullable: true })
|
||||||
|
fechaPasswordTem?: Date;
|
||||||
|
|
||||||
|
@Column({ length: 100, unique: true })
|
||||||
|
correo: string;
|
||||||
|
|
||||||
|
@Column({ length: 100, nullable: true })
|
||||||
|
carreraAds?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'float', default: 0 })
|
||||||
|
cantidadCuenta: number;
|
||||||
|
|
||||||
|
@Column({ length: 150, unique: true })
|
||||||
|
usuario: string;
|
||||||
|
|
||||||
|
@Column({ length: 50, nullable: true })
|
||||||
|
numeroIdentificar?: string;
|
||||||
|
|
||||||
|
@Column({ length: 50 })
|
||||||
|
tipoUsuario: string;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
correoEnviado: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
fechaRegistro: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
cambioPasswordReq: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
primerLogin: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
ExecutionContext,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GoogleAuthGuard extends AuthGuard('google') {
|
||||||
|
handleRequest(err, user, info, context: ExecutionContext) {
|
||||||
|
if (err || !user) {
|
||||||
|
throw err || new UnauthorizedException(); // deja que el controller maneje el redirect
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PersonaService } from './persona.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||||
|
constructor(
|
||||||
|
private readonly personaService: PersonaService,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
) {
|
||||||
|
const clientID = config.get<string>('GOOGLE_CLIENT_ID');
|
||||||
|
const clientSecret = config.get<string>('GOOGLE_CLIENT_SECRET');
|
||||||
|
const callbackURL = config.get<string>('GOOGLE_CALLBACK_URL');
|
||||||
|
|
||||||
|
if (!clientID || !clientSecret || !callbackURL) {
|
||||||
|
throw new Error(
|
||||||
|
'Las variables de entorno de Google OAuth no están definidas',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
super({
|
||||||
|
clientID,
|
||||||
|
clientSecret,
|
||||||
|
callbackURL,
|
||||||
|
scope: ['email', 'profile'],
|
||||||
|
passReqToCallback: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(
|
||||||
|
req: any,
|
||||||
|
accessToken: string,
|
||||||
|
refreshToken: string,
|
||||||
|
profile: any,
|
||||||
|
done: VerifyCallback,
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const email = profile.emails?.[0]?.value;
|
||||||
|
const name = profile.name?.givenName || '';
|
||||||
|
const apellidoP = profile.name?.familyName || '';
|
||||||
|
|
||||||
|
const user = await this.personaService.validateOrCreateGoogleUser({
|
||||||
|
email,
|
||||||
|
nombre: name,
|
||||||
|
apellidoP,
|
||||||
|
});
|
||||||
|
|
||||||
|
done(null, user);
|
||||||
|
} catch (error) {
|
||||||
|
done(error, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { PersonaController } from './persona.controller';
|
||||||
|
import { PersonaService } from './persona.service';
|
||||||
|
|
||||||
|
describe('PersonaController', () => {
|
||||||
|
let controller: PersonaController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [PersonaController],
|
||||||
|
providers: [PersonaService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<PersonaController>(PersonaController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
@Controller('persona')
|
||||||
|
export class PersonaController {
|
||||||
|
constructor(private readonly personaService: PersonaService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getAll() {
|
||||||
|
return this.personaService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirige a Google
|
||||||
|
@Get('google')
|
||||||
|
@UseGuards(GoogleAuthGuard)
|
||||||
|
async googleLogin() {
|
||||||
|
// NestJS + Passport se encarga de redirigir a Google
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback después de autenticación en Google
|
||||||
|
@Get('google/callback')
|
||||||
|
@UseGuards(GoogleAuthGuard)
|
||||||
|
async googleCallback(@Req() req, @Res() res: Response) {
|
||||||
|
const googleUser = req.user;
|
||||||
|
|
||||||
|
if (!googleUser) {
|
||||||
|
return res.redirect(`${process.env.FRONTEND_URL}?error=oauth_failed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar en la base de datos si ya existe
|
||||||
|
let persona = await this.personaService.findByEmail(googleUser.email);
|
||||||
|
|
||||||
|
if (!persona) {
|
||||||
|
// Si no existe, crear nueva persona con los datos de Google
|
||||||
|
persona = await this.personaService.create({
|
||||||
|
nombre: googleUser.nombre || '',
|
||||||
|
apellidoP: googleUser.apellidoP || '',
|
||||||
|
apellidoM: '', // Google no da este dato
|
||||||
|
password: '', // vacío porque es login OAuth
|
||||||
|
passwordTem: '',
|
||||||
|
fechaPasswordTem: new Date(),
|
||||||
|
correo: googleUser.email,
|
||||||
|
carreraAds: '',
|
||||||
|
cantidadCuenta: 0,
|
||||||
|
usuario: '',
|
||||||
|
numeroIdentificar: '',
|
||||||
|
tipoUsuario: '',
|
||||||
|
correoEnviado: false,
|
||||||
|
fechaRegistro: new Date(),
|
||||||
|
cambioPasswordReq: true,
|
||||||
|
primerLogin: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Genera JWT con los datos de la persona
|
||||||
|
const jwt = await this.personaService.generateJwt(persona);
|
||||||
|
|
||||||
|
return res.redirect(
|
||||||
|
`${process.env.FRONTEND_URL}/oauth-callback?token=${jwt.access_token}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Persona]),
|
||||||
|
PassportModule,
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
signOptions: { expiresIn }, // aquí ya es un número
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [PersonaController],
|
||||||
|
providers: [PersonaService, GoogleStrategy],
|
||||||
|
})
|
||||||
|
export class PersonaModule {}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { PersonaService } from './persona.service';
|
||||||
|
|
||||||
|
describe('PersonaService', () => {
|
||||||
|
let service: PersonaService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [PersonaService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<PersonaService>(PersonaService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Persona } from './entities/persona.entity';
|
||||||
|
@Injectable()
|
||||||
|
export class PersonaService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Persona)
|
||||||
|
private readonly personaRepository: Repository<Persona>,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll() {
|
||||||
|
return this.personaRepository.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmail(email: string): Promise<Persona | null> {
|
||||||
|
return this.personaRepository.findOne({ where: { correo: email } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<Persona>): Promise<Persona> {
|
||||||
|
const persona = this.personaRepository.create(data);
|
||||||
|
return this.personaRepository.save(persona);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateJwt(user: Persona) {
|
||||||
|
const payload = { sub: user.idPersona, correo: user.correo };
|
||||||
|
return {
|
||||||
|
access_token: this.jwtService.sign(payload),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opción: validateOrCreateGoogleUser para usar en GoogleStrategy
|
||||||
|
async validateOrCreateGoogleUser(data: {
|
||||||
|
email: string;
|
||||||
|
nombre: string;
|
||||||
|
apellidoP: string;
|
||||||
|
}) {
|
||||||
|
let persona = await this.findByEmail(data.email);
|
||||||
|
|
||||||
|
if (!persona) {
|
||||||
|
persona = await this.create({
|
||||||
|
nombre: data.nombre || '',
|
||||||
|
apellidoP: data.apellidoP || '',
|
||||||
|
apellidoM: '',
|
||||||
|
password: '',
|
||||||
|
passwordTem: '',
|
||||||
|
fechaPasswordTem: new Date(),
|
||||||
|
correo: data.email,
|
||||||
|
carreraAds: '',
|
||||||
|
cantidadCuenta: 0,
|
||||||
|
usuario: '',
|
||||||
|
numeroIdentificar: '',
|
||||||
|
tipoUsuario: '',
|
||||||
|
correoEnviado: false,
|
||||||
|
fechaRegistro: new Date(),
|
||||||
|
cambioPasswordReq: true,
|
||||||
|
primerLogin: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return persona;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user