Se agregaron los campos de los que se deben de mandar a origen de usuario
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "carga_api",
|
||||
"dockerComposeFile": [
|
||||
"../infra/docker-compose.yml"
|
||||
],
|
||||
"service": "api",
|
||||
"workspaceFolder": "/app",
|
||||
"runServices": [
|
||||
"database"
|
||||
],
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "bash"
|
||||
},
|
||||
"appPort": [
|
||||
3051
|
||||
],
|
||||
"shutdownAction": "stopCompose",
|
||||
"remoteUser": "node"
|
||||
}
|
||||
+7
-6
@@ -12,7 +12,7 @@ import { MovimientoModule } from './movimiento/movimiento.module';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
@@ -20,7 +20,7 @@ import { MovimientoModule } from './movimiento/movimiento.module';
|
||||
useFactory: async (config: ConfigService) => {
|
||||
const dbConfig = {
|
||||
type: 'mysql' as const,
|
||||
host: config.get('DB_HOST', 'localhost'),
|
||||
host: config.get('DB_HOST', 'database'),
|
||||
port: config.get<number>('DB_PORT', 3306),
|
||||
username: config.get('DB_USER', 'root'),
|
||||
// no prints en claro en prod:
|
||||
@@ -40,18 +40,19 @@ import { MovimientoModule } from './movimiento/movimiento.module';
|
||||
logger: 'advanced-console',
|
||||
retryAttempts: 5,
|
||||
retryDelay: 2000,
|
||||
dropSchema: true, // solo para desarrollo
|
||||
};
|
||||
},
|
||||
}),
|
||||
TypeOrmModule.forFeature(Object.values(entities)),
|
||||
|
||||
|
||||
ExcelModule,
|
||||
AuthModule,
|
||||
MovimientoModule
|
||||
ExcelModule,
|
||||
AuthModule,
|
||||
MovimientoModule
|
||||
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule { }
|
||||
|
||||
@@ -8,19 +8,22 @@ import { ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
@Controller()
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
constructor(private readonly authService: AuthService) { }
|
||||
|
||||
@Post('login')
|
||||
@AuthDocumentation.login()
|
||||
async login(@Body() dto: LoginDto) {
|
||||
const user = await this.authService.validateUser(dto.email, dto.password);
|
||||
if (!user) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
return this.authService.login(user);
|
||||
}
|
||||
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@ApiBearerAuth('bearer')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('profile')
|
||||
@AuthDocumentation.bearerAuth()
|
||||
getProfile(@Request() req) {
|
||||
@@ -34,7 +37,7 @@ export class AuthController {
|
||||
|
||||
|
||||
|
||||
@Post('register')
|
||||
@Post('register')
|
||||
@AuthDocumentation.register() // ver siguiente sección
|
||||
async register(@Body() dto: RegisterDto) {
|
||||
return this.authService.register(dto);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBody, ApiConsumes, ApiResponse } from '@nestjs/swagger';
|
||||
import { Origen } from 'src/entities/entities';
|
||||
|
||||
export class AuthDocumentation {
|
||||
/**
|
||||
@@ -63,44 +64,41 @@ export class AuthDocumentation {
|
||||
|
||||
|
||||
static register() {
|
||||
return applyDecorators(
|
||||
ApiTags('Auth'),
|
||||
ApiOperation({
|
||||
summary: 'Registrar usuario',
|
||||
description: 'Crea un nuevo usuario con correo y contraseña.'
|
||||
}),
|
||||
ApiConsumes('application/json'),
|
||||
ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string', format: 'email', example: 'nuevo@dominio.com' },
|
||||
password: { type: 'string', example: 'Password123' },
|
||||
nombre: { type: 'string', example: 'Miguel' },
|
||||
a_paterno: { type: 'string', example: 'Cuadra' },
|
||||
a_materno: { type: 'string', example: 'Chavez' },
|
||||
rfc: { type: 'string', example: 'CAHM000343DG2' },
|
||||
},
|
||||
required: ['email','password','nombre','a_paterno','a_materno']
|
||||
}
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 201,
|
||||
description: 'Usuario creado correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_usuario: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'nuevo@dominio.com' },
|
||||
nombre: { type: 'string', example: 'Miguel' },
|
||||
a_paterno: { type: 'string', example: 'Cuadra' },
|
||||
a_materno: { type: 'string', example: 'Chavez' },
|
||||
rfc: { type: 'string', example: 'CAHM000343DG2' },
|
||||
return applyDecorators(
|
||||
ApiTags('Auth'),
|
||||
ApiOperation({
|
||||
summary: 'Registrar usuario',
|
||||
description: 'Crea un nuevo usuario con correo y contraseña.'
|
||||
}),
|
||||
ApiConsumes('application/json'),
|
||||
ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string', format: 'email', example: 'nuevo@dominio.com' },
|
||||
password: { type: 'string', example: 'Password123' },
|
||||
origen: { type: 'string', example: 'redes', description: 'Origen del usuario' }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 409, description: 'El correo ya está registrado.' }),
|
||||
);
|
||||
}
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 201,
|
||||
description: 'Usuario creado correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_usuario: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'nuevo@dominio.com' },
|
||||
|
||||
origen: { type: 'string', example: 'redes' }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 409, description: 'El correo ya está registrado.' }),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ import { ConfigService, ConfigModule } from '@nestjs/config';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { Usuario } from '../entities/entities';
|
||||
import { Origen, UsuariosDelSistema } from '../entities/entities';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Usuario]),
|
||||
TypeOrmModule.forFeature([UsuariosDelSistema, Origen]),
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
@@ -25,4 +25,4 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
providers: [AuthService, JwtStrategy],
|
||||
controllers: [AuthController],
|
||||
})
|
||||
export class AuthModule {}
|
||||
export class AuthModule { }
|
||||
|
||||
+26
-14
@@ -1,23 +1,26 @@
|
||||
// src/auth/auth.service.ts
|
||||
import { ConflictException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Or, Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Usuario } from '../entities/entities';
|
||||
import { Origen, UsuariosDelSistema } from '../entities/entities';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@InjectRepository(Usuario)
|
||||
private readonly userRepo: Repository<Usuario>,
|
||||
@InjectRepository(UsuariosDelSistema)
|
||||
private readonly userRepo: Repository<UsuariosDelSistema>,
|
||||
@InjectRepository(Origen)
|
||||
private readonly origenRepo: Repository<Origen>,
|
||||
|
||||
private readonly jwtService: JwtService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
// 1) Valida email/password
|
||||
async validateUser(email: string, pass: string): Promise<Usuario> {
|
||||
const user = await this.userRepo.findOne({ where: { correo: email } });
|
||||
async validateUser(email: string, pass: string): Promise<UsuariosDelSistema> {
|
||||
const user = await this.userRepo.findOne({ where: { correo: email }, relations: ['origen'] });
|
||||
if (!user) throw new UnauthorizedException('Credenciales inválidas');
|
||||
const match = await bcrypt.compare(pass, user.contraseña);
|
||||
if (!match) throw new UnauthorizedException('Credenciales inválidas');
|
||||
@@ -25,8 +28,12 @@ export class AuthService {
|
||||
}
|
||||
|
||||
// 2) Genera el JWT
|
||||
async login(user: Usuario) {
|
||||
const payload = { sub: user.id_usuario, email: user.correo };
|
||||
async login(user: UsuariosDelSistema) {
|
||||
if (!user)
|
||||
throw new UnauthorizedException('Usuario no encontrado');
|
||||
if (!user.origen)
|
||||
throw new UnauthorizedException('Usuario no encontrado, tiene origen vacío');
|
||||
const payload = { sub: user.id_usuario, email: user.correo, tipo: user.origen.origen };
|
||||
return {
|
||||
access_token: this.jwtService.sign(payload),
|
||||
};
|
||||
@@ -45,15 +52,20 @@ export class AuthService {
|
||||
// 2) Hashear contraseña
|
||||
const hash = await bcrypt.hash(dto.password, 10);
|
||||
|
||||
let origen = await this.origenRepo.findOne({ where: { origen: dto.origen } });
|
||||
|
||||
if (!origen) {
|
||||
origen = this.origenRepo.create({ origen: dto.origen });
|
||||
origen = await this.origenRepo.save(origen);
|
||||
}
|
||||
|
||||
|
||||
// 3) Crear entidad Usuario
|
||||
const user = this.userRepo.create({
|
||||
correo: dto.email,
|
||||
contraseña: hash,
|
||||
nombre: dto.nombre,
|
||||
a_paterno: dto.a_paterno,
|
||||
a_materno: dto.a_materno,
|
||||
rfc: dto.rfc ?? null,
|
||||
// otros campos necesarios (fecha_nacimiento, generacion…) o valores por defecto
|
||||
origen: origen,
|
||||
|
||||
});
|
||||
|
||||
// 4) Guardar en BD
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsEmail, IsString, MinLength, MaxLength, IsOptional } from 'class-validator';
|
||||
import { IsEmail, IsString, MinLength, MaxLength, IsOptional, IsNumber } from 'class-validator';
|
||||
|
||||
export class RegisterDto {
|
||||
@IsEmail()
|
||||
@@ -8,20 +8,8 @@ export class RegisterDto {
|
||||
@MinLength(6)
|
||||
password: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(60)
|
||||
nombre: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(60)
|
||||
a_paterno: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(60)
|
||||
a_materno: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(15)
|
||||
rfc?: string;
|
||||
origen: string;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
|
||||
async validate(payload: any) {
|
||||
// Devuelve lo que estará disponible en Request.user
|
||||
return { userId: payload.sub, email: payload.email };
|
||||
return { userId: payload.sub, email: payload.email, origen: payload.tipo };
|
||||
}
|
||||
}
|
||||
|
||||
+30
-36
@@ -6,7 +6,7 @@ export class Origen {
|
||||
id_origen: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 30 })
|
||||
fuente: string;
|
||||
origen: string;
|
||||
|
||||
@OneToMany(() => Movimiento, movimiento => movimiento.origen)
|
||||
movimientos: Movimiento[];
|
||||
@@ -17,7 +17,7 @@ export class Origen {
|
||||
|
||||
@Entity({ name: 'genero' })
|
||||
export class Genero {
|
||||
@PrimaryGeneratedColumn({ name: 'id_genero', type: 'int', })
|
||||
@PrimaryGeneratedColumn({ name: 'id_genero', type: 'int', })
|
||||
id_genero: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, nullable: true })
|
||||
@@ -47,14 +47,14 @@ export class Usuario {
|
||||
@PrimaryGeneratedColumn({ name: 'id_usuario', type: 'int' })
|
||||
id_usuario: number;
|
||||
|
||||
@Column({
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 9,
|
||||
unique: true,
|
||||
nullable: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
num_cuenta: string;
|
||||
num_cuenta: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 60 })
|
||||
nombre: string;
|
||||
@@ -65,36 +65,24 @@ export class Usuario {
|
||||
@Column({ name: 'a_materno', type: 'varchar', length: 60 })
|
||||
a_materno: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 15,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
rfc: string | null;
|
||||
@Column({ name: 'fecha_nacimiento', type: 'varchar', nullable: true, length: 8 })
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 15,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
rfc: string | null;
|
||||
|
||||
@Column({ name: 'fecha_nacimiento', type: 'varchar', nullable: true, length: 8 })
|
||||
fecha_nacimiento: string | null;
|
||||
|
||||
@Column({ type: 'int', nullable: true, })
|
||||
@Column({ type: 'int', nullable: true, })
|
||||
generacion: number | null;
|
||||
|
||||
@ManyToOne(() => Genero, genero => genero.usuarios, { nullable: true })
|
||||
@JoinColumn({ name: 'id_genero' })
|
||||
genero: Genero | null;
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 100, // ajusta longitud según tu necesidad
|
||||
unique: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
correo: string | null;
|
||||
@ManyToOne(() => Genero, genero => genero.usuarios, { nullable: true })
|
||||
@JoinColumn({ name: 'id_genero' })
|
||||
genero: Genero | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 60 })
|
||||
contraseña: string;
|
||||
|
||||
@OneToMany(() => Movimiento, mov => mov.usuario)
|
||||
movimientos: Movimiento[];
|
||||
|
||||
@OneToMany(() => CarreraUsuario, cu => cu.usuario)
|
||||
carreraUsuarios: CarreraUsuario[];
|
||||
@@ -105,8 +93,17 @@ correo: string | null;
|
||||
|
||||
@Entity({ name: 'usuariosDelSistema' })
|
||||
export class UsuariosDelSistema {
|
||||
@PrimaryColumn({ type: 'varchar', length: 60 })
|
||||
usuario: string;
|
||||
@PrimaryGeneratedColumn({ name: 'id_usuario', type: 'int' })
|
||||
id_usuario: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 100, // ajusta longitud según tu necesidad
|
||||
unique: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
correo: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 60 })
|
||||
contraseña: string;
|
||||
@@ -189,16 +186,13 @@ export class Movimiento {
|
||||
@Column({ type: 'bit' })
|
||||
flag: boolean;
|
||||
|
||||
@Column({
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 200,
|
||||
nullable: true, // permite NULL
|
||||
})
|
||||
reporte?: string;
|
||||
|
||||
@ManyToOne(() => Usuario, usuario => usuario.movimientos)
|
||||
@JoinColumn({ name: 'id_usuario' })
|
||||
usuario: Usuario;
|
||||
|
||||
@ManyToOne(() => Origen, origen => origen.movimientos)
|
||||
@JoinColumn({ name: 'id_origen' })
|
||||
|
||||
+130
-29
@@ -3,7 +3,7 @@ import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterc
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { Response } from 'express';
|
||||
import e, { Response } from 'express';
|
||||
import { ExcelDocumentation } from './excel.documentation';
|
||||
import { ExcelService } from './excel.service';
|
||||
import { MovimientoService } from '../movimiento/movimiento.service';
|
||||
@@ -15,7 +15,7 @@ export class ExcelController {
|
||||
constructor(
|
||||
private readonly excelService: ExcelService,
|
||||
private readonly movimientoService: MovimientoService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
@Post('verify')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@@ -24,7 +24,7 @@ export class ExcelController {
|
||||
const userId: number = req.user.userId; // ahora sí existe
|
||||
const errors = await this.excelService.validateFile(file.buffer);
|
||||
await this.movimientoService.log(
|
||||
userId,
|
||||
|
||||
'VERIFY',
|
||||
errors.length ? 'FAILED' : 'SUCCESS',
|
||||
errors.join('; ')
|
||||
@@ -35,12 +35,20 @@ export class ExcelController {
|
||||
@Post('load')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ExcelDocumentation.loadExcel()
|
||||
async loadExcel(@UploadedFile() file: Express.Multer.File, @Request() req) {
|
||||
const userId: number = req.user.userId;
|
||||
async loadExcel(@Request() req, @UploadedFile() file: Express.Multer.File) {
|
||||
const origen = req.user.origen; // ahora sí existe
|
||||
if (origen != 'LOAD') {
|
||||
await this.movimientoService.log(
|
||||
'LOAD',
|
||||
'FAILED',
|
||||
'Origen no permitido para carga'
|
||||
);
|
||||
throw new Error('Origen no permitido para carga.');
|
||||
}
|
||||
try {
|
||||
const result = await this.excelService.loadFile(file.buffer);
|
||||
await this.movimientoService.log(
|
||||
userId,
|
||||
|
||||
'LOAD',
|
||||
'SUCCESS',
|
||||
undefined,
|
||||
@@ -49,7 +57,7 @@ export class ExcelController {
|
||||
return result;
|
||||
} catch (err) {
|
||||
await this.movimientoService.log(
|
||||
userId,
|
||||
|
||||
'LOAD',
|
||||
'FAILED',
|
||||
err.message
|
||||
@@ -61,31 +69,124 @@ export class ExcelController {
|
||||
@Get('download')
|
||||
@ExcelDocumentation.downloadExcel()
|
||||
async downloadData(@Request() req, @Res() res: Response) {
|
||||
const userId: number = req.user.userId;
|
||||
try {
|
||||
const csv = await this.excelService.generateCsv();
|
||||
const origen = req.user.origen; // ahora sí existe
|
||||
console.log('Origen de descarga:', origen);
|
||||
console.log('Usuario:', req.user);
|
||||
|
||||
if (origen == 'SOLICITA') {
|
||||
try {
|
||||
const csv = await this.excelService.generateCsvSolicita();
|
||||
await this.movimientoService.log(
|
||||
|
||||
origen,
|
||||
'SUCCESS',
|
||||
undefined,
|
||||
`size=${csv.length}`
|
||||
);
|
||||
return res
|
||||
.status(HttpStatus.OK)
|
||||
.header('Content-Type', 'text/tab-separated-values')
|
||||
.header('Content-Disposition', 'attachment; filename="usuarios_solicita.tsv"')
|
||||
.send(csv);
|
||||
} catch (err) {
|
||||
await this.movimientoService.log(
|
||||
origen,
|
||||
'FAILED',
|
||||
'Origen no permitido para descarga'
|
||||
);
|
||||
return res
|
||||
.status(HttpStatus.FORBIDDEN)
|
||||
.json({ statusCode: 403, message: 'Origen no permitido para descarga.' });
|
||||
}
|
||||
} else if (origen == 'CORREO') {
|
||||
try {
|
||||
const csv = await this.excelService.generateCsvCorreo();
|
||||
await this.movimientoService.log(
|
||||
|
||||
origen,
|
||||
'SUCCESS',
|
||||
undefined,
|
||||
`size=${csv.length}`
|
||||
);
|
||||
return res
|
||||
.status(HttpStatus.OK)
|
||||
.header('Content-Type', 'text/tab-separated-values')
|
||||
.header('Content-Disposition', 'attachment; filename="usuarios.tsv"')
|
||||
.send(csv);
|
||||
} catch (err) {
|
||||
await this.movimientoService.log(
|
||||
|
||||
origen,
|
||||
'FAILED',
|
||||
err.message
|
||||
);
|
||||
return res
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.json({ statusCode: 500, message: 'Error generando descarga.' });
|
||||
}
|
||||
} else if (origen == 'AT') {
|
||||
try {
|
||||
const csv = await this.excelService.generateCsvAT();
|
||||
await this.movimientoService.log(
|
||||
|
||||
origen,
|
||||
'SUCCESS',
|
||||
undefined,
|
||||
`size=${csv.length}`
|
||||
);
|
||||
return res
|
||||
.status(HttpStatus.OK)
|
||||
.header('Content-Type', 'text/tab-separated-values')
|
||||
.header('Content-Disposition', 'attachment; filename="usuarios_at.tsv"')
|
||||
.send(csv);
|
||||
} catch (err) {
|
||||
await this.movimientoService.log(
|
||||
|
||||
origen,
|
||||
'FAILED',
|
||||
err.message
|
||||
);
|
||||
return res
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.json({ statusCode: 500, message: 'Error generando descarga.' });
|
||||
}
|
||||
} else if (origen == 'RED') {
|
||||
try {
|
||||
const csv = await this.excelService.generateCsvRed();
|
||||
await this.movimientoService.log(
|
||||
|
||||
origen,
|
||||
'SUCCESS',
|
||||
undefined,
|
||||
`size=${csv.length}`
|
||||
);
|
||||
return res
|
||||
.status(HttpStatus.OK)
|
||||
.header('Content-Type', 'text/tab-separated-values')
|
||||
.header('Content-Disposition', 'attachment; filename="usuarios_red.tsv"')
|
||||
.send(csv);
|
||||
} catch (err) {
|
||||
await this.movimientoService.log(
|
||||
|
||||
origen,
|
||||
'FAILED',
|
||||
err.message
|
||||
);
|
||||
return res
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.json({ statusCode: 500, message: 'Error generando descarga.' });
|
||||
}
|
||||
} else {
|
||||
await this.movimientoService.log(
|
||||
userId,
|
||||
'DOWNLOAD',
|
||||
'SUCCESS',
|
||||
undefined,
|
||||
`size=${csv.length}`
|
||||
);
|
||||
return res
|
||||
.status(HttpStatus.OK)
|
||||
.header('Content-Type', 'text/tab-separated-values')
|
||||
.header('Content-Disposition', 'attachment; filename="usuarios.tsv"')
|
||||
.send(csv);
|
||||
} catch (err) {
|
||||
await this.movimientoService.log(
|
||||
userId,
|
||||
'DOWNLOAD',
|
||||
|
||||
origen,
|
||||
'FAILED',
|
||||
err.message
|
||||
'Origen no permitido para descarga'
|
||||
);
|
||||
return res
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.json({ statusCode: 500, message: 'Error generando descarga.' });
|
||||
.status(HttpStatus.FORBIDDEN)
|
||||
.json({ statusCode: 403, message: 'Origen no permitido para descarga.' });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+332
-76
@@ -1,7 +1,7 @@
|
||||
// src/excel/excel.service.ts
|
||||
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||
import { Workbook } from 'exceljs';
|
||||
import { Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Usuario } from '../entities/entities';
|
||||
import { Genero } from '../entities/entities';
|
||||
@@ -35,7 +35,7 @@ export class ExcelService {
|
||||
@InjectRepository(Carrera)
|
||||
private readonly carreraRepo: Repository<Carrera>,
|
||||
// … inyecta otros repositorios si los usarás
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/** Lee el buffer del Excel y devuelve un arreglo de filas tipadas */
|
||||
private async parseFile(buffer: Buffer): Promise<UsuarioRow[]> {
|
||||
@@ -92,54 +92,54 @@ export class ExcelService {
|
||||
}
|
||||
|
||||
/** Valida duplicados, celdas vacías y patrones básicos */
|
||||
validateRows(rows: UsuarioRow[]) {
|
||||
const errors: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
validateRows(rows: UsuarioRow[]) {
|
||||
const errors: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
rows.forEach((r, i) => {
|
||||
const rowNum = i + 2; // por el encabezado
|
||||
rows.forEach((r, i) => {
|
||||
const rowNum = i + 2; // por el encabezado
|
||||
|
||||
// Campos que no pueden quedar vacíos
|
||||
['cuenta','nombreCompleto','clave','fechnac'].forEach(field => {
|
||||
const raw = (r as any)[field];
|
||||
const str = raw != null ? raw.toString().trim() : '';
|
||||
if (!str) {
|
||||
errors.push(`Fila ${rowNum}: campo "${field}" está vacío.`);
|
||||
// Campos que no pueden quedar vacíos
|
||||
['cuenta', 'nombreCompleto'].forEach(field => {
|
||||
const raw = (r as any)[field];
|
||||
const str = raw != null ? raw.toString().trim() : '';
|
||||
if (!str) {
|
||||
errors.push(`Fila ${rowNum}: campo "${field}" está vacío.`);
|
||||
}
|
||||
});
|
||||
|
||||
// Duplicados de "cuenta"
|
||||
const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : '';
|
||||
if (seen.has(cuentaStr)) {
|
||||
errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`);
|
||||
} else {
|
||||
seen.add(cuentaStr);
|
||||
}
|
||||
|
||||
// Numérico en cuenta y generación
|
||||
|
||||
|
||||
const genStr = r.gen != null ? r.gen.toString().trim() : '';
|
||||
if (genStr && !/^[0-9]{4}$/.test(genStr)) {
|
||||
errors.push(`Fila ${rowNum}: generación inválida "${genStr}".`);
|
||||
}
|
||||
|
||||
|
||||
// Fecha en formato YYYYMMDD
|
||||
const fechaStr = r.fechnac != null ? r.fechnac.toString().trim() : '';
|
||||
if (!/^[0-9]{8}$/.test(fechaStr)) {
|
||||
errors.push(`Fila ${rowNum}: fecha de nacimiento inválida "${fechaStr}".`);
|
||||
}
|
||||
|
||||
// RFC alfanumérico
|
||||
const rfcStr = r.rfc != null ? r.rfc.toString().trim() : '';
|
||||
if (rfcStr && !/^[A-Z0-9]+$/.test(rfcStr)) {
|
||||
errors.push(`Fila ${rowNum}: RFC contiene caracteres no válidos.`);
|
||||
}
|
||||
});
|
||||
|
||||
// Duplicados de "cuenta"
|
||||
const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : '';
|
||||
if (seen.has(cuentaStr)) {
|
||||
errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`);
|
||||
} else {
|
||||
seen.add(cuentaStr);
|
||||
}
|
||||
|
||||
// Numérico en cuenta y generación
|
||||
if (!/^[0-9]+$/.test(cuentaStr)) {
|
||||
errors.push(`Fila ${rowNum}: cuenta debe ser numérica.`);
|
||||
}
|
||||
const genStr = r.gen != null ? r.gen.toString().trim() : '';
|
||||
if (!/^[0-9]{4}$/.test(genStr)) {
|
||||
errors.push(`Fila ${rowNum}: generación inválida "${genStr}".`);
|
||||
}
|
||||
|
||||
// Fecha en formato YYYYMMDD
|
||||
const fechaStr = r.fechnac != null ? r.fechnac.toString().trim() : '';
|
||||
if (!/^[0-9]{8}$/.test(fechaStr)) {
|
||||
errors.push(`Fila ${rowNum}: fecha de nacimiento inválida "${fechaStr}".`);
|
||||
}
|
||||
|
||||
// RFC alfanumérico
|
||||
const rfcStr = r.rfc != null ? r.rfc.toString().trim() : '';
|
||||
if (rfcStr && !/^[A-Z0-9]+$/.test(rfcStr)) {
|
||||
errors.push(`Fila ${rowNum}: RFC contiene caracteres no válidos.`);
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -181,6 +181,9 @@ export class ExcelService {
|
||||
await this.carreraRepo.save(carrera);
|
||||
}
|
||||
|
||||
const generacion = /^[0-9]{4}$/.test(r.gen?.toString().trim())
|
||||
? parseInt(r.gen.toString().trim(), 10)
|
||||
: null;
|
||||
// 3) Usuario
|
||||
const usuario = this.usuarioRepo.create({
|
||||
num_cuenta: r.cuenta,
|
||||
@@ -189,8 +192,9 @@ export class ExcelService {
|
||||
a_materno: r.apellidoma,
|
||||
rfc: r.rfc,
|
||||
fecha_nacimiento: r.fechnac,
|
||||
generacion: parseInt(r.gen, 10),
|
||||
generacion: generacion,
|
||||
genero,
|
||||
|
||||
});
|
||||
await this.usuarioRepo.save(usuario);
|
||||
|
||||
@@ -207,48 +211,300 @@ export class ExcelService {
|
||||
|
||||
|
||||
|
||||
// async generateCsv(): Promise<string> {
|
||||
// const users = await this.usuarioRepo.find({
|
||||
// relations: ['genero', 'carreraUsuarios'], // si quieres más info
|
||||
// select: ['num_cuenta', 'nombre', 'a_paterno', 'a_materno', 'rfc', 'fecha_nacimiento', 'generacion'],
|
||||
// });
|
||||
|
||||
// const header = [
|
||||
// 'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
|
||||
// 'carrera', 'rfc', 'fecha_nacimiento', 'generacion'
|
||||
// ].join('\t') + '\n';
|
||||
|
||||
// const rows = users.map(u => [
|
||||
// u.num_cuenta,
|
||||
// u.nombre,
|
||||
// u.a_paterno,
|
||||
// u.a_materno,
|
||||
// u.rfc ?? '',
|
||||
// u.fecha_nacimiento,
|
||||
// (u.generacion != null ? u.generacion.toString() : '')
|
||||
|
||||
// ].join('\t')).join('\n');
|
||||
|
||||
// return header + rows;
|
||||
// }
|
||||
|
||||
|
||||
async generateCsvCorreo(): Promise<string> {
|
||||
|
||||
|
||||
|
||||
const tipos = [
|
||||
'Diplomado',
|
||||
'Extra Largo',
|
||||
'Ampliación de Conocimiento',
|
||||
'Servicio Social',
|
||||
'Movilidad',
|
||||
'Intercambio UNAM',
|
||||
'Idiomas Sabatino',
|
||||
'Idiomas R (UNAM)',
|
||||
'Trabajadores',
|
||||
'Profesor',
|
||||
'Oyente',
|
||||
'Posgrado',
|
||||
'Reinscrito',
|
||||
'Licenciatura',
|
||||
];
|
||||
|
||||
const usuarios = await this.usuarioRepo.find({
|
||||
where: {
|
||||
usuarioTipos: {
|
||||
tipoUsuario: {
|
||||
tipo_usuario: In(tipos),
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: [
|
||||
'genero',
|
||||
'carreraUsuarios', // relación intermedia
|
||||
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
|
||||
],
|
||||
select: {
|
||||
num_cuenta: true,
|
||||
nombre: true,
|
||||
a_paterno: true,
|
||||
a_materno: true,
|
||||
rfc: true,
|
||||
fecha_nacimiento: true,
|
||||
generacion: true,
|
||||
carreraUsuarios: true,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async generateCsv(): Promise<string> {
|
||||
const users = await this.usuarioRepo.find({
|
||||
relations: ['genero', 'carreraUsuarios'], // si quieres más info
|
||||
select: [ 'num_cuenta','nombre','a_paterno','a_materno','correo','rfc','fecha_nacimiento','generacion' ],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const header = [
|
||||
'num_cuenta','nombre','a_paterno','a_materno',
|
||||
'correo','rfc','fecha_nacimiento','generacion'
|
||||
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
|
||||
'carreras', 'rfc', 'fecha_nacimiento', 'generacion'
|
||||
].join('\t') + '\n';
|
||||
|
||||
const rows = users.map(u => [
|
||||
u.num_cuenta,
|
||||
u.nombre,
|
||||
u.a_paterno,
|
||||
u.a_materno,
|
||||
u.correo ?? '',
|
||||
u.rfc ?? '',
|
||||
u.fecha_nacimiento,
|
||||
(u.generacion != null ? u.generacion.toString() : '')
|
||||
const rows = usuarios.map(u => {
|
||||
const carreras = (u.carreraUsuarios ?? [])
|
||||
.map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre
|
||||
.join(', '); // une todas las carreras en un solo string
|
||||
|
||||
].join('\t')).join('\n');
|
||||
return [
|
||||
u.num_cuenta,
|
||||
u.nombre,
|
||||
u.a_paterno,
|
||||
u.a_materno,
|
||||
carreras,
|
||||
u.rfc ?? '',
|
||||
u.fecha_nacimiento,
|
||||
u.generacion?.toString() ?? ''
|
||||
].join('\t');
|
||||
}).join('\n');
|
||||
|
||||
return header + rows;
|
||||
const tablaCompleta = header + rows;
|
||||
|
||||
return tablaCompleta;
|
||||
}
|
||||
|
||||
async generateCsvSolicita(): Promise<string> {
|
||||
|
||||
|
||||
|
||||
const tipos = [
|
||||
|
||||
'Extra Largo',
|
||||
'Ampliación de Conocimiento',
|
||||
'Movilidad',
|
||||
'Intercambio UNAM',
|
||||
'Profesor',
|
||||
'Posgrado',
|
||||
'Reinscrito',
|
||||
'Licenciatura',
|
||||
];
|
||||
|
||||
const usuarios = await this.usuarioRepo.find({
|
||||
where: {
|
||||
usuarioTipos: {
|
||||
tipoUsuario: {
|
||||
tipo_usuario: In(tipos),
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: [
|
||||
'genero',
|
||||
'carreraUsuarios', // relación intermedia
|
||||
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
|
||||
],
|
||||
select: {
|
||||
num_cuenta: true,
|
||||
nombre: true,
|
||||
a_paterno: true,
|
||||
a_materno: true,
|
||||
rfc: true,
|
||||
carreraUsuarios: true,
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const header = [
|
||||
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
|
||||
'carreras', 'rfc'
|
||||
].join('\t') + '\n';
|
||||
|
||||
const rows = usuarios.map(u => {
|
||||
const carreras = (u.carreraUsuarios ?? [])
|
||||
.map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre
|
||||
.join(', '); // une todas las carreras en un solo string
|
||||
|
||||
return [
|
||||
u.num_cuenta,
|
||||
u.nombre,
|
||||
u.a_paterno,
|
||||
u.a_materno,
|
||||
carreras,
|
||||
u.rfc ?? '',
|
||||
].join('\t');
|
||||
}).join('\n');
|
||||
|
||||
const tablaCompleta = header + rows;
|
||||
|
||||
return tablaCompleta;
|
||||
}
|
||||
|
||||
async generateCsvRed(): Promise<string> {
|
||||
|
||||
|
||||
|
||||
const tipos = [
|
||||
'Ampliación de Conocimiento',
|
||||
'Servicio Social',
|
||||
'Movilidad',
|
||||
'Intercambio UNAM',
|
||||
'Idiomas Sabatino',
|
||||
'Idiomas R (UNAM)',
|
||||
'Trabajadores',
|
||||
'Profesor',
|
||||
'Posgrado',
|
||||
'Reinscrito',
|
||||
'Licenciatura',
|
||||
];
|
||||
|
||||
const usuarios = await this.usuarioRepo.find({
|
||||
where: {
|
||||
usuarioTipos: {
|
||||
tipoUsuario: {
|
||||
tipo_usuario: In(tipos),
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: [
|
||||
'genero',
|
||||
'carreraUsuarios', // relación intermedia
|
||||
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
|
||||
],
|
||||
select: {
|
||||
num_cuenta: true,
|
||||
fecha_nacimiento: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const header = [
|
||||
'num_cuenta', 'fecha_nacimiento'
|
||||
].join('\t') + '\n';
|
||||
|
||||
const rows = usuarios.map(u => {
|
||||
return [
|
||||
u.num_cuenta,
|
||||
u.fecha_nacimiento,
|
||||
].join('\t');
|
||||
}).join('\n');
|
||||
|
||||
const tablaCompleta = header + rows;
|
||||
|
||||
return tablaCompleta;
|
||||
}
|
||||
|
||||
async generateCsvAT(): Promise<string> {
|
||||
|
||||
|
||||
|
||||
const tipos = [
|
||||
'Ampliación de Conocimiento',
|
||||
'Servicio Social',
|
||||
'Movilidad',
|
||||
'Intercambio UNAM',
|
||||
'Profesor',
|
||||
'Posgrado',
|
||||
'Reinscrito',
|
||||
'Licenciatura',
|
||||
];
|
||||
|
||||
const usuarios = await this.usuarioRepo.find({
|
||||
where: {
|
||||
usuarioTipos: {
|
||||
tipoUsuario: {
|
||||
tipo_usuario: In(tipos),
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: [
|
||||
'genero',
|
||||
'carreraUsuarios', // relación intermedia
|
||||
'carreraUsuarios.carrera', // carrera asociada a través de la intermedia
|
||||
],
|
||||
select: {
|
||||
num_cuenta: true,
|
||||
nombre: true,
|
||||
a_paterno: true,
|
||||
a_materno: true,
|
||||
fecha_nacimiento: true,
|
||||
generacion: true,
|
||||
carreraUsuarios: true,
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const header = [
|
||||
'num_cuenta', 'nombre', 'a_paterno', 'a_materno',
|
||||
'carreras', 'rfc', 'fecha_nacimiento', 'generacion'
|
||||
].join('\t') + '\n';
|
||||
|
||||
const rows = usuarios.map(u => {
|
||||
const carreras = (u.carreraUsuarios ?? [])
|
||||
.map(cu => cu.carrera?.carrera ?? '') // obtenemos solo el nombre
|
||||
.join(', '); // une todas las carreras en un solo string
|
||||
|
||||
return [
|
||||
u.num_cuenta,
|
||||
u.nombre,
|
||||
u.a_paterno,
|
||||
u.a_materno,
|
||||
carreras,
|
||||
u.fecha_nacimiento,
|
||||
u.generacion?.toString() ?? ''
|
||||
].join('\t');
|
||||
}).join('\n');
|
||||
|
||||
const tablaCompleta = header + rows;
|
||||
|
||||
return tablaCompleta;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Movimiento } from '../entities/entities';
|
||||
import { Usuario } from '../entities/entities';
|
||||
import { Origen } from '../entities/entities';
|
||||
|
||||
@Injectable()
|
||||
@@ -12,20 +11,19 @@ export class MovimientoService {
|
||||
private readonly movRepo: Repository<Movimiento>,
|
||||
@InjectRepository(Origen)
|
||||
private readonly origenRepo: Repository<Origen>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/** Crea un registro de movimiento */
|
||||
async log(
|
||||
usuarioId: number,
|
||||
fuente: string,
|
||||
origen: string,
|
||||
status: string,
|
||||
observaciones?: string,
|
||||
reporte?: string,
|
||||
flag = false,
|
||||
) {
|
||||
// 1) Obtén el origen o falla si no existe
|
||||
const origen = await this.origenRepo.findOne({ where: { fuente } });
|
||||
if (!origen) throw new Error(`Origen desconocido: ${fuente}`);
|
||||
const origin = await this.origenRepo.findOne({ where: { origen } });
|
||||
if (!origin) throw new Error(`Origen desconocido: ${origen}`);
|
||||
|
||||
// 2) Inserta directamente usando los campos de FK
|
||||
await this.movRepo.insert({
|
||||
@@ -34,8 +32,7 @@ export class MovimientoService {
|
||||
observaciones, // aquí usas el valor que vino como parámetro
|
||||
reporte, // idem
|
||||
flag,
|
||||
usuario: { id_usuario: usuarioId }, // relación en lugar de id_usuario
|
||||
origen: { id_origen: origen.id_origen },
|
||||
origen: { id_origen: origin.id_origen },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user