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": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@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/typeorm": "^11.0.0",
|
||||
"mysql2": "^3.15.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
@@ -35,6 +44,8 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/passport-google-oauth20": "^2.0.16",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
|
||||
+25
-1
@@ -1,9 +1,33 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { PersonaModule } from './persona/persona.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
@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],
|
||||
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