From 3f9df94b8fc0c393d678b4966af18bbad41791f5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 13 Jun 2025 16:10:25 -0600 Subject: [PATCH 01/22] infra folder, swagger init, error handler --- infra/Dockerfile | 21 ++++++++++++++++++ infra/docker-compose.yml | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 infra/Dockerfile create mode 100644 infra/docker-compose.yml diff --git a/infra/Dockerfile b/infra/Dockerfile new file mode 100644 index 0000000..e647f20 --- /dev/null +++ b/infra/Dockerfile @@ -0,0 +1,21 @@ +# Usa una imagen de Node.js 22 basada en Alpine +FROM node:22.10.0-alpine + +# Establece el directorio de trabajo +WORKDIR /app + +# Instala las dependencias para compilar bcrypt en Alpine +RUN apk add --no-cache python3 make g++ + +# Copia los archivos de dependencias y los instala +COPY package.json package-lock.json ./ +RUN npm install + +# Copia el resto del código fuente +COPY . . + +# Expone el puerto 3000 +EXPOSE 3051 + +# Comando por defecto para iniciar la aplicación en desarrollo +CMD ["npm", "run", "start:dev"] \ No newline at end of file diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml new file mode 100644 index 0000000..9d9406b --- /dev/null +++ b/infra/docker-compose.yml @@ -0,0 +1,47 @@ +services: + database: + image: mariadb:latest + container_name: carga_db + restart: always + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: carga_test + MYSQL_USER: user + MYSQL_PASSWORD: password + ports: + - "3356:3306" + volumes: + - mariadb_data:/var/lib/mysql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + - ./init_insert_data.sql:/docker-entrypoint-initdb.d/init_insert_data.sql + + + networks: + - carga_network + + api: + + build: + context: .. + dockerfile: infra/Dockerfile + + container_name: carga_api + + restart: always + depends_on: + - database + env_file: + - ../.env + + ports: + - "3051:3051" + networks: + - carga_network + + + +volumes: + mariadb_data: + +networks: + carga_network: \ No newline at end of file From 8660c85b79f0ff459e54572cf4a1fa4ffb790481 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 13 Jun 2025 17:53:16 -0600 Subject: [PATCH 02/22] primera instancia de validador y carga del excel de usuarios, falta agregar las relaciones con las carreras y verificar los validadores, --- infra/docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 9d9406b..098ba99 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -6,8 +6,6 @@ services: environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: carga_test - MYSQL_USER: user - MYSQL_PASSWORD: password ports: - "3356:3306" volumes: From 4deb5ecf343b4b7a73a0481278e26dc978bae440 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 13 Jun 2025 17:54:38 -0600 Subject: [PATCH 03/22] primera instancia de validador y carga del excel de usuarios, falta agregar las relaciones con las carreras y verificar los validadores --- example_envi.txt | 10 + package-lock.json | 1472 ++++++++++++++++++++++++++++-- package.json | 17 +- src/app.module.ts | 46 +- src/entities/entities.ts | 177 ++++ src/excel/excel.controller.ts | 34 + src/excel/excel.documentation.ts | 96 ++ src/excel/excel.module.ts | 14 + src/excel/excel.service.ts | 207 +++++ src/helpers/exception.filter.ts | 54 ++ src/main.ts | 49 +- 11 files changed, 2083 insertions(+), 93 deletions(-) create mode 100644 example_envi.txt create mode 100644 src/entities/entities.ts create mode 100644 src/excel/excel.controller.ts create mode 100644 src/excel/excel.documentation.ts create mode 100644 src/excel/excel.module.ts create mode 100644 src/excel/excel.service.ts create mode 100644 src/helpers/exception.filter.ts diff --git a/example_envi.txt b/example_envi.txt new file mode 100644 index 0000000..deeba02 --- /dev/null +++ b/example_envi.txt @@ -0,0 +1,10 @@ +API_PORT=3051 + + +DB_HOST=database +DB_PORT=3306 +DB_USER=root +DB_PASS=root +DB_NAME=carga_test +DB_SYNC=true +DB_LOGGING=true \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bc7f2cf..8885980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,20 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", - "@nestjs/platform-express": "^11.0.1", + "@nestjs/platform-express": "^11.1.3", + "@nestjs/swagger": "^11.2.0", + "@nestjs/typeorm": "^11.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "dotenv": "^16.5.0", + "exceljs": "^4.4.0", + "multer": "^2.0.1", + "mysql2": "^3.14.1", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "typeorm": "^0.3.24" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -25,7 +35,8 @@ "@swc/core": "^1.10.7", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", - "@types/node": "^22.10.7", + "@types/multer": "^1.4.13", + "@types/node": "^22.15.31", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -732,7 +743,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -745,7 +756,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -906,6 +917,47 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1294,7 +1346,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -1312,7 +1363,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1325,14 +1375,12 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -1350,7 +1398,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -1860,7 +1907,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1891,7 +1938,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -1914,6 +1961,12 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, "node_modules/@napi-rs/nice": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", @@ -2465,6 +2518,33 @@ } } }, + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@nestjs/core": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.3.tgz", @@ -2506,6 +2586,26 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.3.tgz", @@ -2625,6 +2725,39 @@ "tslib": "^2.1.0" } }, + "node_modules/@nestjs/swagger": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.0.tgz", + "integrity": "sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "8.2.0", + "swagger-ui-dist": "5.21.0" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.3.tgz", @@ -2653,6 +2786,19 @@ } } }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", + "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -2730,6 +2876,16 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", @@ -2743,6 +2899,13 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -2790,6 +2953,12 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, "node_modules/@swc/cli": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.6.0.tgz", @@ -3128,28 +3297,28 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/babel__core": { @@ -3362,11 +3531,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.15.31", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz", "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3440,6 +3619,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.15.1", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.1.tgz", + "integrity": "sha512-9gG6ogYcoI2mCMLdcO0NYI0AYrbxIjv0MDmy/5Ywo6CpWWrqYayc+mmgxRsCgtcGJm9BSbXkMsmxGah1iGHAAQ==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -4466,7 +4651,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4489,7 +4674,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -4597,7 +4782,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -4610,7 +4794,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4626,7 +4809,6 @@ "version": "3.17.0", "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -4659,6 +4841,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -4686,18 +4877,123 @@ ], "license": "MIT" }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/archiver/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-timsort": { @@ -4718,7 +5014,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, "license": "MIT" }, "node_modules/asynckit": { @@ -4728,6 +5023,15 @@ "dev": true, "license": "MIT" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -4865,7 +5169,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/bare-events": { @@ -4880,7 +5183,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -4897,6 +5199,15 @@ ], "license": "MIT" }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/bin-version": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", @@ -4932,11 +5243,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -4944,6 +5267,12 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -4968,7 +5297,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -5048,7 +5376,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -5073,7 +5400,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -5085,6 +5411,23 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -5204,6 +5547,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5287,6 +5642,23 @@ "dev": true, "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5343,7 +5715,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -5358,7 +5729,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5368,7 +5738,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -5381,7 +5750,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -5427,7 +5795,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5440,7 +5807,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -5493,11 +5859,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -5582,7 +5962,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -5625,6 +6004,31 @@ } } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -5651,14 +6055,13 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5669,6 +6072,12 @@ "node": ">= 8" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -5719,7 +6128,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -5780,6 +6188,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5814,7 +6231,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -5830,6 +6247,33 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5844,11 +6288,49 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/ee-first": { @@ -5897,7 +6379,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -5909,6 +6390,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -5990,7 +6480,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6250,6 +6739,44 @@ "node": ">=0.8.x" } }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exceljs/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/exceljs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6416,6 +6943,19 @@ "node": ">=0.10.0" } }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6711,7 +7251,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -6838,6 +7377,12 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -6864,7 +7409,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -6882,6 +7426,22 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6891,6 +7451,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -6905,7 +7474,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -7096,7 +7664,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -7271,6 +7838,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -7323,7 +7896,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -7392,7 +7964,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7457,6 +8028,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7483,11 +8060,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -8287,7 +8869,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8370,6 +8951,48 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8400,6 +9023,48 @@ "node": ">=6" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -8424,6 +9089,21 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.9.tgz", + "integrity": "sha512-VWwAdNeJgN7jFOD+wN4qx83DTPMVPPAUyx9/TUkBXKLiNkuWWk6anV0439tgdtwaJDrEdqkvdN22iA6J4bUCZg==", + "license": "MIT" + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8431,6 +9111,12 @@ "dev": true, "license": "MIT" }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, "node_modules/load-esm": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", @@ -8480,7 +9166,73 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", "license": "MIT" }, "node_modules/lodash.memoize": { @@ -8497,6 +9249,18 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -8514,6 +9278,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lowercase-keys": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -8537,6 +9307,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -8567,7 +9352,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/makeerror": { @@ -8738,7 +9523,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -8760,7 +9544,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -8855,6 +9638,47 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mysql2": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz", + "integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8913,7 +9737,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9134,9 +9957,14 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9192,7 +10020,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9202,7 +10029,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9468,6 +10294,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9626,6 +10458,36 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -9660,7 +10522,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9795,6 +10656,40 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -9870,6 +10765,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -9977,6 +10884,11 @@ "node": ">= 18" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -10002,17 +10914,35 @@ "node": ">= 18" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -10025,7 +10955,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10107,7 +11036,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -10197,6 +11125,31 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -10301,7 +11254,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10317,7 +11269,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10332,7 +11283,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10342,7 +11292,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10355,7 +11304,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10365,7 +11313,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10378,7 +11325,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -10395,7 +11341,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10408,7 +11353,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10535,6 +11479,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -10863,6 +11816,15 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -10974,7 +11936,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -11117,11 +12079,222 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/typeorm": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.24.tgz", + "integrity": "sha512-4IrHG7A0tY8l5gEGXfW56VOMfUVWEkWlH/h5wmcyZ+V8oCiLj7iTPp0lEjMEZVrxEkGSdP9ErgTKHKXQApl/oA==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^3.17.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.13", + "debug": "^4.4.0", + "dedent": "^1.6.0", + "dotenv": "^16.4.7", + "glob": "^10.4.5", + "sha.js": "^2.4.11", + "sql-highlight": "^6.0.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", + "@sap/hana-client": "^2.12.25", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "hdb-pool": "^0.1.6", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0", + "reflect-metadata": "^0.1.14 || ^0.2.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "hdb-pool": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/typeorm/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -11193,7 +12366,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -11215,6 +12388,54 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -11262,11 +12483,24 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -11284,6 +12518,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -11544,7 +12787,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -11586,7 +12828,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -11604,7 +12845,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11614,7 +12854,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11673,6 +12912,12 @@ "dev": true, "license": "ISC" }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -11686,7 +12931,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -11703,7 +12947,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -11722,7 +12965,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -11746,7 +12988,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -11777,6 +13019,62 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } } } } diff --git a/package.json b/package.json index 141d859..e1b6519 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,20 @@ }, "dependencies": { "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", - "@nestjs/platform-express": "^11.0.1", + "@nestjs/platform-express": "^11.1.3", + "@nestjs/swagger": "^11.2.0", + "@nestjs/typeorm": "^11.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "dotenv": "^16.5.0", + "exceljs": "^4.4.0", + "multer": "^2.0.1", + "mysql2": "^3.14.1", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "typeorm": "^0.3.24" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -36,7 +46,8 @@ "@swc/core": "^1.10.7", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", - "@types/node": "^22.10.7", + "@types/multer": "^1.4.13", + "@types/node": "^22.15.31", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", diff --git a/src/app.module.ts b/src/app.module.ts index 8662803..17a34aa 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,9 +1,51 @@ -import { Module } from '@nestjs/common'; +import { Logger, Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import * as entities from './entities/entities'; +import { ExcelModule } from './excel/excel.module'; + + @Module({ - imports: [], + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (config: ConfigService) => { + const dbConfig = { + type: 'mysql' as const, + host: config.get('DB_HOST', 'localhost'), + port: config.get('DB_PORT', 3306), + username: config.get('DB_USER', 'root'), + // no prints en claro en prod: + password: config.get('DB_PASS', ''), + database: config.get('DB_NAME', 'test'), + }; + Logger.log('[DB CONFIG] ' + JSON.stringify({ + ...dbConfig, + password: '***' + }), 'TypeORM'); + return { + ...dbConfig, + entities: Object.values(entities), + synchronize: config.get('DB_SYNC', true), + logging: ['error', 'warn', 'info', 'query', 'schema'], + logger: 'advanced-console', + retryAttempts: 5, + retryDelay: 2000, + }; + }, + }), + TypeOrmModule.forFeature(Object.values(entities)), + + + ExcelModule, + + ], controllers: [AppController], providers: [AppService], }) diff --git a/src/entities/entities.ts b/src/entities/entities.ts new file mode 100644 index 0000000..c5a676c --- /dev/null +++ b/src/entities/entities.ts @@ -0,0 +1,177 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn, PrimaryColumn } from 'typeorm'; + +@Entity({ name: 'origen' }) +export class Origen { + @PrimaryGeneratedColumn({ name: 'id_origen', type: 'int' }) + id_origen: number; + + @Column({ type: 'varchar', length: 30 }) + fuente: string; + + @OneToMany(() => Movimiento, movimiento => movimiento.origen) + movimientos: Movimiento[]; + + @OneToMany(() => UsuariosDelSistema, uds => uds.origen) + usuariosDelSistema: UsuariosDelSistema[]; +} + +@Entity({ name: 'genero' }) +export class Genero { + @PrimaryGeneratedColumn({ name: 'id_genero', type: 'int', }) + id_genero: number; + + @Column({ type: 'varchar', length: 20, nullable: true }) + genero: string; + + @OneToMany(() => Usuario, usuario => usuario.genero) + usuarios: Usuario[]; +} + +@Entity({ name: 'carrera' }) +export class Carrera { + @PrimaryGeneratedColumn({ name: 'id_carrera', type: 'int' }) + id_carrera: number; + + @Column({ type: 'varchar', length: 100 }) + carrera: string; + + @Column({ type: 'varchar', length: 6 }) + clave: string; + + @OneToMany(() => CarreraUsuario, cu => cu.carrera) + carreraUsuarios: CarreraUsuario[]; +} + +@Entity({ name: 'usuario' }) +export class Usuario { + @PrimaryGeneratedColumn({ name: 'id_usuario', type: 'int' }) + id_usuario: number; + + @Column({ type: 'varchar', length: 9 }) + num_cuenta: string; + + @Column({ type: 'varchar', length: 60 }) + nombre: string; + + @Column({ name: 'a_paterno', type: 'varchar', length: 60 }) + a_paterno: string; + + @Column({ name: 'a_materno', type: 'varchar', length: 60 }) + a_materno: string; + + @Column({ type: 'varchar', length: 15 }) + rfc: string; + + @Column({ name: 'fecha_nacimiento', type: 'varchar', length: 8 }) + fecha_nacimiento: string; + + @Column({ type: 'int' }) + generacion: number; + + @ManyToOne(() => Genero, genero => genero.usuarios) + @JoinColumn({ name: 'id_genero' }) + genero: Genero; + + @OneToMany(() => Movimiento, mov => mov.usuario) + movimientos: Movimiento[]; + + @OneToMany(() => CarreraUsuario, cu => cu.usuario) + carreraUsuarios: CarreraUsuario[]; + + @OneToMany(() => UsuarioTipoUsuario, utu => utu.usuario) + usuarioTipos: UsuarioTipoUsuario[]; +} + +@Entity({ name: 'usuariosDelSistema' }) +export class UsuariosDelSistema { + @PrimaryColumn({ type: 'varchar', length: 60 }) + usuario: string; + + @Column({ type: 'varchar', length: 60 }) + contraseña: string; + + @ManyToOne(() => Origen, origen => origen.usuariosDelSistema) + @JoinColumn({ name: 'id_origen' }) + origen: Origen; +} + +@Entity({ name: 'carrera_usuario' }) +export class CarreraUsuario { + @PrimaryGeneratedColumn({ name: 'id_carrera_usuario', type: 'int' }) + id_carrera_usuario: number; + + @ManyToOne(() => Carrera, carrera => carrera.carreraUsuarios) + @JoinColumn({ name: 'id_carrera' }) + carrera: Carrera; + + @ManyToOne(() => Usuario, usuario => usuario.carreraUsuarios) + @JoinColumn({ name: 'id_usuario' }) + usuario: Usuario; +} + +@Entity({ name: 'equipo' }) +export class Equipo { + @PrimaryGeneratedColumn({ name: 'id_equipo', type: 'int' }) + id_equipo: number; + + @Column({ type: 'varchar', length: 10 }) + equipo: string; + + @Column({ name: 'numero_serie', type: 'varchar', length: 30 }) + numero_serie: string; +} + +@Entity({ name: 'tipo_usuario' }) +export class TipoUsuario { + @PrimaryGeneratedColumn({ name: 'id_tipo_usuario', type: 'int' }) + id_tipo_usuario: number; + + @Column({ name: 'tipo_usuario', type: 'varchar', length: 20 }) + tipo_usuario: string; + + @OneToMany(() => UsuarioTipoUsuario, utu => utu.tipoUsuario) + usuariosTipos: UsuarioTipoUsuario[]; +} + +@Entity({ name: 'usuario_tipo_usuario' }) +export class UsuarioTipoUsuario { + @PrimaryGeneratedColumn({ name: 'id_user_tipo_usuario', type: 'int' }) + id_user_tipo_usuario: number; + + @ManyToOne(() => TipoUsuario, tipo => tipo.usuariosTipos) + @JoinColumn({ name: 'id_tipo_usuario' }) + tipoUsuario: TipoUsuario; + + @ManyToOne(() => Usuario, usuario => usuario.usuarioTipos) + @JoinColumn({ name: 'id_usuario' }) + usuario: Usuario; +} + +@Entity({ name: 'movimiento' }) +export class Movimiento { + @PrimaryGeneratedColumn({ name: 'id_mov', type: 'int' }) + id_mov: number; + + @Column({ name: 'fecha_mov', type: 'datetime' }) + fecha_mov: Date; + + @Column({ type: 'varchar', length: 100 }) + status: string; + + @Column({ type: 'varchar', length: 200 }) + observaciones: string; + + @Column({ type: 'bit' }) + flag: boolean; + + @Column({ type: 'varchar', length: 200 }) + reporte: string; + + @ManyToOne(() => Usuario, usuario => usuario.movimientos) + @JoinColumn({ name: 'id_usuario' }) + usuario: Usuario; + + @ManyToOne(() => Origen, origen => origen.movimientos) + @JoinColumn({ name: 'id_origen' }) + origen: Origen; +} diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts new file mode 100644 index 0000000..7c6d7d0 --- /dev/null +++ b/src/excel/excel.controller.ts @@ -0,0 +1,34 @@ +// src/excel/excel.controller.ts +import { + Controller, + Post, + UploadedFile, + UseInterceptors, + BadRequestException, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ExcelService } from './excel.service'; +import { ExcelDocumentation } from './excel.documentation'; + +@Controller('excel') +export class ExcelController { + constructor(private readonly excelService: ExcelService) {} + + @Post('verify') + @UseInterceptors(FileInterceptor('file')) + @ExcelDocumentation.verifyExcel() + async verifyExcel(@UploadedFile() file: Express.Multer.File) { + if (!file) throw new BadRequestException('Se requiere un archivo .xlsx'); + const errors = await this.excelService.validateFile(file.buffer); + return { valid: errors.length === 0, errors }; + } + + @Post('load') + @UseInterceptors(FileInterceptor('file')) + @ExcelDocumentation.loadExcel() + async loadExcel(@UploadedFile() file: Express.Multer.File) { + if (!file) throw new BadRequestException('Se requiere un archivo .xlsx'); + const result = await this.excelService.loadFile(file.buffer); + return result; // { inserted: X } + } +} diff --git a/src/excel/excel.documentation.ts b/src/excel/excel.documentation.ts new file mode 100644 index 0000000..41b5f02 --- /dev/null +++ b/src/excel/excel.documentation.ts @@ -0,0 +1,96 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiConsumes, ApiBody, ApiResponse } from '@nestjs/swagger'; + +export class ExcelDocumentation { + /** + * Decorators Swagger para el endpoint POST /excel/verify + */ + static verifyExcel() { + return applyDecorators( + ApiTags('Excel'), + ApiOperation({ + summary: 'Verificar archivo Excel', + description: 'Valida duplicados, filas en blanco y formato básico de los datos sin realizar carga en la base.' + }), + ApiConsumes('multipart/form-data'), + ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + description: 'Archivo Excel (.xlsx) con usuarios a validar' + } + } + } + }), + ApiResponse({ + status: 200, + description: 'Resultado de la validación', + schema: { + type: 'object', + properties: { + valid: { type: 'boolean', example: false }, + errors: { + type: 'array', + items: { type: 'string' }, + example: ['Fila 2: cuenta duplicada "42515101".', 'Fila 3: fecha de nacimiento inválida "1988051".'] + } + } + } + }), + ApiResponse({ status: 400, description: 'No se recibió archivo o formato inválido.' }) + ); + } + + /** + * Decorators Swagger para el endpoint POST /excel/load + */ + static loadExcel() { + return applyDecorators( + ApiTags('Excel'), + ApiOperation({ + summary: 'Cargar archivo Excel', + description: 'Valida y persiste los datos en la base de datos. Retorna el número de registros insertados.' + }), + ApiConsumes('multipart/form-data'), + ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + description: 'Archivo Excel (.xlsx) con usuarios a cargar' + } + } + } + }), + ApiResponse({ + status: 201, + description: 'Usuarios insertados correctamente', + schema: { + type: 'object', + properties: { + inserted: { type: 'number', example: 3 } + } + } + }), + ApiResponse({ + status: 400, + description: 'Errores de validación o carga', + schema: { + type: 'object', + properties: { + errors: { + type: 'array', + items: { type: 'string' }, + example: ['Fila 5: rfc contiene caracteres inválidos.'] + } + } + } + }) + ); + } +} diff --git a/src/excel/excel.module.ts b/src/excel/excel.module.ts new file mode 100644 index 0000000..c4eec57 --- /dev/null +++ b/src/excel/excel.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ExcelService } from './excel.service'; +import { ExcelController } from './excel.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Usuario, Genero, Carrera } from '../entities/entities'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Usuario, Genero, Carrera]), + ], + providers: [ExcelService], + controllers: [ExcelController], +}) +export class ExcelModule {} diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts new file mode 100644 index 0000000..5950085 --- /dev/null +++ b/src/excel/excel.service.ts @@ -0,0 +1,207 @@ +// src/excel/excel.service.ts +import { Injectable, BadRequestException, Logger } from '@nestjs/common'; +import { Workbook } from 'exceljs'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Usuario } from '../entities/entities'; +import { Genero } from '../entities/entities'; +import { Carrera } from '../entities/entities'; + +export interface UsuarioRow { + cuenta: string; + nombreCompleto: string; + clave: string; + nomCarr: string; + gen: string; // generación + fechnac: string; // YYYYMMDD + apellidopa: string; + apellidoma: string; + nombres: string; + sexo: string; // m/f o texto + tipo: string; + correo: string; + rfc: string; +} + +@Injectable() +export class ExcelService { + private readonly logger = new Logger(ExcelService.name); + + constructor( + @InjectRepository(Usuario) + private readonly usuarioRepo: Repository, + @InjectRepository(Genero) + private readonly generoRepo: Repository, + @InjectRepository(Carrera) + private readonly carreraRepo: Repository, + // … 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 { + const wb = new Workbook(); + await wb.xlsx.load(buffer); + const sheet = wb.worksheets[0]; + const rows: UsuarioRow[] = []; + + // Asume que la primera fila es encabezados + sheet.eachRow((row, idx) => { + if (idx === 1) return; // salto encabezados + + // 1) Asegurarnos de que row.values no sea null/undefined + if (!row.values) { + this.logger.warn(`Fila ${idx + 1}: row.values vacío, se omite.`); + return; + } + + // 2) row.values[0] es null, así que slice(1) sí existe + const [ + cuenta, + nombreCompleto, + clave, + nomCarr, + gen, + fechnac, + apellidopa, + apellidoma, + nombres, + sexo, + tipo, + correo, + rfc, + ] = (row.values as any[]).slice(1); + + rows.push({ + cuenta, + nombreCompleto, + clave, + nomCarr, + gen, + fechnac, + apellidopa, + apellidoma, + nombres, + sexo, + tipo, + correo, + rfc, + }); + }); + + return rows; + } + + /** Valida duplicados, celdas vacías y patrones básicos */ + validateRows(rows: UsuarioRow[]) { + const errors: string[] = []; + const seen = new Set(); + + 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.`); + } + }); + + // 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; +} + + + /** + * Solo valida: retorna lista de errores (vacío = ok) + */ + async validateFile(buffer: Buffer) { + const rows = await this.parseFile(buffer); + const errs = this.validateRows(rows); + return errs; + } + + /** + * Valida y luego persiste usuarios y relaciones básicas + */ + async loadFile(buffer: Buffer) { + const rows = await this.parseFile(buffer); + const errs = this.validateRows(rows); + if (errs.length) { + throw new BadRequestException({ errors: errs }); + } + + for (const r of rows) { + // 1) Género (crea o reutiliza) + let genero = await this.generoRepo.findOne({ where: { genero: r.sexo } }); + if (!genero) { + genero = this.generoRepo.create({ genero: r.sexo }); + await this.generoRepo.save(genero); + } + + // 2) Carrera (por clave) + let carrera = await this.carreraRepo.findOne({ + where: { clave: r.clave }, + }); + if (!carrera) { + carrera = this.carreraRepo.create({ + clave: r.clave, + carrera: r.nomCarr.slice(0, 100), + }); + await this.carreraRepo.save(carrera); + } + + // 3) Usuario + const usuario = this.usuarioRepo.create({ + num_cuenta: r.cuenta, + nombre: r.nombres.trim().split(' ')[0], // ajusta si quieres + a_paterno: r.apellidopa, + a_materno: r.apellidoma, + rfc: r.rfc, + fecha_nacimiento: r.fechnac, + generacion: parseInt(r.gen, 10), + genero, + }); + await this.usuarioRepo.save(usuario); + + // 4) Relacion usuario–carrera (tabla intermedia) + await this.carreraRepo + .createQueryBuilder() + .relation('carreraUsuarios') + .of(carrera) + .add(usuario); + } + + return { inserted: rows.length }; + } +} diff --git a/src/helpers/exception.filter.ts b/src/helpers/exception.filter.ts new file mode 100644 index 0000000..12a8305 --- /dev/null +++ b/src/helpers/exception.filter.ts @@ -0,0 +1,54 @@ +import { + Catch, + ArgumentsHost, + ExceptionFilter, + HttpException, +} from '@nestjs/common'; +import { Response } from 'express'; + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + catch(exception: any, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + let status = 500; // Valor por defecto para errores no controlados + let message = 'Internal server error'; + + if (exception instanceof HttpException) { + status = exception.getStatus(); + const responseBody = exception.getResponse(); + + // Evitar el problema de circular structure usando JSON-safe conversion + message = + typeof responseBody === 'object' + ? this.safeStringify(responseBody) + : (responseBody as string); + } else { + // Puedes mapear el error a un código HTTP y mensaje adecuados + status = 400; + message = (exception as any).sqlMessage || exception.message; + + console.error('Unhandled Exception:', exception); // Log de errores no controlados + } + + response.status(status).json({ + statusCode: status, + message, + timestamp: new Date().toISOString(), + path: ctx.getRequest().url, + }); + } + + // ✅ Método para manejar objetos circulares usando WeakSet + private safeStringify(obj: any): string { + const seen = new WeakSet(); + return JSON.stringify(obj, (key, value) => { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) return '[Circular]'; + seen.add(value); + } + return value; + }); + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index f76bc8d..5871d92 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,55 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { ValidationPipe } from '@nestjs/common'; +import { GlobalExceptionFilter } from './helpers/exception.filter'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(process.env.PORT ?? 3000); + app.enableCors(); + + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + }), + ); + + app.useGlobalFilters(new GlobalExceptionFilter()); + + const config = new DocumentBuilder() + .setTitle('Documentacion de la api de carga masiva de CEDETEC') + .setDescription( + 'El objetivo es mejorar la gestion interna y la integridad de los datos', + ) + .setVersion('1.0') + .addBearerAuth() + .build(); + // .addServer(process.env.SWAGGER_SERVER_URL) // descomentar parael servidor de acatlan + + const document = SwaggerModule.createDocument(app, config); + + SwaggerModule.setup('documentation', app, document); + + const port = process.env.API_PORT || 4000; + await app.listen(port, '0.0.0.0'); + + console.log( + `[main] Aplicación iniciada en el puerto ${port} a las ${new Date().toISOString()}`, + ); + console.log( + `\n\x1b[36m=========================================================\x1b[0m`, + ); + console.log(`\x1b[36m SISTEMA DE CARGA MASIVA - API\x1b[0m`); + console.log( + `\x1b[36m=========================================================\x1b[0m`, + ); + console.log(`\x1b[32m✅ API corriendo en:\x1b[0m http://localhost:${port}`); + console.log( + `\x1b[32m✅ Documentación disponible en:\x1b[0m http://localhost:${port}/documentation`, + ); + + console.log( + `\x1b[36m=========================================================\x1b[0m\n`, + ); } bootstrap(); From e39a148adc1f3f4831369df52576963e18d8ad1a Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 15 Jun 2025 20:53:51 -0600 Subject: [PATCH 04/22] registro, login y verificacion de informacion de usuario --- package-lock.json | 256 ++++++++++++++++++++++++++++++++- package.json | 7 + src/app.module.ts | 3 + src/auth/auth.controller.ts | 41 ++++++ src/auth/auth.documentation.ts | 106 ++++++++++++++ src/auth/auth.module.ts | 28 ++++ src/auth/auth.service.ts | 66 +++++++++ src/auth/dto/login.dto.ts | 11 ++ src/auth/dto/register.dto.ts | 27 ++++ src/auth/jwt.strategy.ts | 27 ++++ src/entities/entities.ts | 27 +++- 11 files changed, 590 insertions(+), 9 deletions(-) create mode 100644 src/auth/auth.controller.ts create mode 100644 src/auth/auth.documentation.ts create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/auth.service.ts create mode 100644 src/auth/dto/login.dto.ts create mode 100644 src/auth/dto/register.dto.ts create mode 100644 src/auth/jwt.strategy.ts diff --git a/package-lock.json b/package-lock.json index 8885980..5818d64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,15 +12,20 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.1.3", "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", + "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "dotenv": "^16.5.0", "exceljs": "^4.4.0", "multer": "^2.0.1", "mysql2": "^3.14.1", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "typeorm": "^0.3.24" @@ -33,10 +38,12 @@ "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", + "@types/bcrypt": "^5.0.2", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/multer": "^1.4.13", "@types/node": "^22.15.31", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -2586,6 +2593,19 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", + "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.7", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", @@ -2606,6 +2626,16 @@ } } }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.3.tgz", @@ -3366,6 +3396,16 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -3517,6 +3557,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -3545,12 +3594,43 @@ "version": "22.15.31", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz", "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "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, + "license": "MIT", + "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", @@ -5199,6 +5279,20 @@ ], "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -5405,6 +5499,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6333,6 +6433,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -8951,6 +9060,28 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -8993,6 +9124,27 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9198,6 +9350,12 @@ "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -9217,18 +9375,36 @@ "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", "license": "MIT" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, "node_modules/lodash.isnil": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", "license": "MIT" }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "license": "MIT" }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.isundefined": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", @@ -9249,6 +9425,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -9709,6 +9891,15 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.4.0.tgz", + "integrity": "sha512-D9DI/gXHvVmjHS08SVch0Em8G5S1P+QWtU31appcKT/8wFSPRcdHadIFSAntdMMVM5zz+/DL+bL/gz3UDppqtg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -9719,6 +9910,17 @@ "lodash": "^4.17.21" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10006,6 +10208,42 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10087,6 +10325,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/peek-readable": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz", @@ -10824,7 +11067,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12366,7 +12608,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -12483,6 +12724,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/package.json b/package.json index e1b6519..77685cf 100644 --- a/package.json +++ b/package.json @@ -23,15 +23,20 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.1.3", "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", + "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "dotenv": "^16.5.0", "exceljs": "^4.4.0", "multer": "^2.0.1", "mysql2": "^3.14.1", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "typeorm": "^0.3.24" @@ -44,10 +49,12 @@ "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", + "@types/bcrypt": "^5.0.2", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/multer": "^1.4.13", "@types/node": "^22.15.31", + "@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/app.module.ts b/src/app.module.ts index 17a34aa..bd770ae 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,6 +7,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import * as entities from './entities/entities'; import { ExcelModule } from './excel/excel.module'; +import { AuthModule } from './auth/auth.module'; @Module({ @@ -33,6 +34,7 @@ import { ExcelModule } from './excel/excel.module'; ...dbConfig, entities: Object.values(entities), synchronize: config.get('DB_SYNC', true), + logging: ['error', 'warn', 'info', 'query', 'schema'], logger: 'advanced-console', retryAttempts: 5, @@ -44,6 +46,7 @@ import { ExcelModule } from './excel/excel.module'; ExcelModule, + AuthModule ], controllers: [AppController], diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..7324fc6 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,41 @@ +import { AuthDocumentation } from './auth.documentation'; +import { Controller, Post, Body, UseGuards, Request, Get } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { LoginDto } from './dto/login.dto'; +import { AuthGuard } from '@nestjs/passport'; +import { RegisterDto } from './dto/register.dto'; + +@Controller() +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Post('login') + @AuthDocumentation.login() + async login(@Body() dto: LoginDto) { + const user = await this.authService.validateUser(dto.email, dto.password); + return this.authService.login(user); + } + + @UseGuards(AuthGuard('jwt')) + @Get('profile') + @AuthDocumentation.bearerAuth() + getProfile(@Request() req) { + // req.user viene de JwtStrategy.validate() + return { + userId: req.user.userId, + email: req.user.email, + }; + } + + + + + @Post('register') + @AuthDocumentation.register() // ver siguiente sección + async register(@Body() dto: RegisterDto) { + return this.authService.register(dto); + } + + + +} diff --git a/src/auth/auth.documentation.ts b/src/auth/auth.documentation.ts new file mode 100644 index 0000000..b3d5b90 --- /dev/null +++ b/src/auth/auth.documentation.ts @@ -0,0 +1,106 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBody, ApiConsumes, ApiResponse } from '@nestjs/swagger'; + +export class AuthDocumentation { + /** + * Decorators Swagger para el endpoint POST /login + */ + static login() { + return applyDecorators( + ApiTags('Auth'), + ApiOperation({ + summary: 'Iniciar sesión', + description: 'Autentica a un usuario con correo y contraseña, retorna un JWT en caso de éxito.' + }), + ApiConsumes('application/json'), + ApiBody({ + schema: { + type: 'object', + properties: { + email: { type: 'string', format: 'email', example: 'usuario@dominio.com' }, + password: { type: 'string', minLength: 6, example: 'miPassword123' } + }, + required: ['email', 'password'] + } + }), + ApiResponse({ + status: 200, + description: 'Autenticación exitosa. Retorna token JWT.', + schema: { + type: 'object', + properties: { + access_token: { type: 'string', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' } + } + } + }), + ApiResponse({ + status: 401, + description: 'Credenciales inválidas.', + schema: { + type: 'object', + properties: { + statusCode: { type: 'integer', example: 401 }, + message: { type: 'string', example: 'Credenciales inválidas' }, + error: { type: 'string', example: 'Unauthorized' } + } + } + }) + ); + } + + /** + * Decorators para proteger rutas con JWT + */ + static bearerAuth() { + return applyDecorators( + ApiOperation({ summary: 'Requiere autenticación JWT' }), + ApiResponse({ status: 401, description: 'Token no provisto o inválido.' }), + ApiResponse({ status: 403, description: 'Token válido pero sin permisos.' }) + ); + } + + + + + 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' }, + } + } + }), + ApiResponse({ status: 409, description: 'El correo ya está registrado.' }), + ); +} + +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..d0b3a47 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,28 @@ +// src/auth/auth.module.ts +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +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 { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Usuario]), + PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (cs: ConfigService) => ({ + secret: cs.get('JWT_SECRET'), + signOptions: { expiresIn: cs.get('JWT_EXPIRES_IN') }, + }), + }), + ], + providers: [AuthService, JwtStrategy], + controllers: [AuthController], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..b49a1ce --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,66 @@ +// src/auth/auth.service.ts +import { ConflictException, Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Usuario } 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, + private readonly jwtService: JwtService, + ) {} + + // 1) Valida email/password + async validateUser(email: string, pass: string): Promise { + const user = await this.userRepo.findOne({ where: { correo: email } }); + 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'); + return user; + } + + // 2) Genera el JWT + async login(user: Usuario) { + const payload = { sub: user.id_usuario, email: user.correo }; + return { + access_token: this.jwtService.sign(payload), + }; + } + + + + + async register(dto: RegisterDto) { + // 1) Verificar duplicado + const exists = await this.userRepo.findOne({ where: { correo: dto.email } }); + if (exists) { + throw new ConflictException('El correo ya está registrado'); + } + + // 2) Hashear contraseña + const hash = await bcrypt.hash(dto.password, 10); + + // 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 + }); + + // 4) Guardar en BD + const saved = await this.userRepo.save(user); + + // 5) Devolver sin contraseña + const { contraseña, ...result } = saved; + return result; + } +} diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts new file mode 100644 index 0000000..9bb4e2f --- /dev/null +++ b/src/auth/dto/login.dto.ts @@ -0,0 +1,11 @@ +// src/auth/dto/login.dto.ts +import { IsEmail, IsString, MinLength } from 'class-validator'; + +export class LoginDto { + @IsEmail() + email: string; + + @IsString() + @MinLength(6) + password: string; +} diff --git a/src/auth/dto/register.dto.ts b/src/auth/dto/register.dto.ts new file mode 100644 index 0000000..3c32c2b --- /dev/null +++ b/src/auth/dto/register.dto.ts @@ -0,0 +1,27 @@ +import { IsEmail, IsString, MinLength, MaxLength, IsOptional } from 'class-validator'; + +export class RegisterDto { + @IsEmail() + email: string; + + @IsString() + @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; +} diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts new file mode 100644 index 0000000..77658ce --- /dev/null +++ b/src/auth/jwt.strategy.ts @@ -0,0 +1,27 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, ExtractJwt, StrategyOptions } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(configService: ConfigService) { + const secret = configService.get('JWT_SECRET'); + if (!secret) { + throw new Error('JWT_SECRET is not defined'); + } + + const opts: StrategyOptions = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: secret, + }; + + super(opts); + } + + async validate(payload: any) { + // Devuelve lo que estará disponible en Request.user + return { userId: payload.sub, email: payload.email }; + } +} diff --git a/src/entities/entities.ts b/src/entities/entities.ts index c5a676c..4f8d4c5 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -59,18 +59,33 @@ export class Usuario { @Column({ name: 'a_materno', type: 'varchar', length: 60 }) a_materno: string; - @Column({ type: 'varchar', length: 15 }) - rfc: string; - +@Column({ + type: 'varchar', + length: 15, + nullable: true, + default: null, +}) +rfc: string | null; @Column({ name: 'fecha_nacimiento', type: 'varchar', length: 8 }) fecha_nacimiento: string; @Column({ type: 'int' }) generacion: number; - @ManyToOne(() => Genero, genero => genero.usuarios) - @JoinColumn({ name: 'id_genero' }) - genero: Genero; + @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; + + @Column({ type: 'varchar', length: 60 }) + contraseña: string; @OneToMany(() => Movimiento, mov => mov.usuario) movimientos: Movimiento[]; From c8ce8f0dfb40068c25f9166999e542c1c58f968a Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 16 Jun 2025 10:24:03 -0600 Subject: [PATCH 05/22] login,registro y auth funcionando --- src/auth/auth.controller.ts | 4 ++++ src/entities/entities.ts | 18 ++++++++++++------ src/main.ts | 11 ++++++++++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 7324fc6..f092d3f 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -4,6 +4,7 @@ import { AuthService } from './auth.service'; import { LoginDto } from './dto/login.dto'; import { AuthGuard } from '@nestjs/passport'; import { RegisterDto } from './dto/register.dto'; +import { ApiBearerAuth } from '@nestjs/swagger'; @Controller() export class AuthController { @@ -16,6 +17,9 @@ export class AuthController { return this.authService.login(user); } + + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth('bearer') @UseGuards(AuthGuard('jwt')) @Get('profile') @AuthDocumentation.bearerAuth() diff --git a/src/entities/entities.ts b/src/entities/entities.ts index 4f8d4c5..8add556 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -47,8 +47,14 @@ export class Usuario { @PrimaryGeneratedColumn({ name: 'id_usuario', type: 'int' }) id_usuario: number; - @Column({ type: 'varchar', length: 9 }) - num_cuenta: string; + @Column({ + type: 'varchar', + length: 9, + unique: true, + nullable: true, + default: null, + }) + num_cuenta: string; @Column({ type: 'varchar', length: 60 }) nombre: string; @@ -66,11 +72,11 @@ export class Usuario { default: null, }) rfc: string | null; - @Column({ name: 'fecha_nacimiento', type: 'varchar', length: 8 }) - fecha_nacimiento: string; + @Column({ name: 'fecha_nacimiento', type: 'varchar', nullable: true, length: 8 }) + fecha_nacimiento: string | null; - @Column({ type: 'int' }) - generacion: number; + @Column({ type: 'int', nullable: true, }) + generacion: number | null; @ManyToOne(() => Genero, genero => genero.usuarios, { nullable: true }) @JoinColumn({ name: 'id_genero' }) diff --git a/src/main.ts b/src/main.ts index 5871d92..96bd98b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,7 +22,16 @@ async function bootstrap() { 'El objetivo es mejorar la gestion interna y la integridad de los datos', ) .setVersion('1.0') - .addBearerAuth() + .addBearerAuth( + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + name: 'Authorization', + in: 'header', + }, + 'bearer', // nombre del security definition + ) .build(); // .addServer(process.env.SWAGGER_SERVER_URL) // descomentar parael servidor de acatlan From fb8991cb3520ef4c554421f77ea291ce53036a3e Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 16 Jun 2025 11:26:17 -0600 Subject: [PATCH 06/22] registro de movimientos de la carga, verificacion y descarga. falta probar --- src/app.module.ts | 4 +- src/entities/entities.ts | 16 +++-- src/excel/excel.controller.ts | 102 +++++++++++++++++++++++++-- src/excel/excel.documentation.ts | 41 +++++++++++ src/excel/excel.module.ts | 2 + src/excel/excel.service.ts | 63 +++++++++++++++++ src/movimiento/movimiento.module.ts | 12 ++++ src/movimiento/movimiento.service.ts | 41 +++++++++++ 8 files changed, 270 insertions(+), 11 deletions(-) create mode 100644 src/movimiento/movimiento.module.ts create mode 100644 src/movimiento/movimiento.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index bd770ae..83172c0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import * as entities from './entities/entities'; import { ExcelModule } from './excel/excel.module'; import { AuthModule } from './auth/auth.module'; +import { MovimientoModule } from './movimiento/movimiento.module'; @Module({ @@ -46,7 +47,8 @@ import { AuthModule } from './auth/auth.module'; ExcelModule, - AuthModule + AuthModule, + MovimientoModule ], controllers: [AppController], diff --git a/src/entities/entities.ts b/src/entities/entities.ts index 8add556..a6f1d06 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -179,14 +179,22 @@ export class Movimiento { @Column({ type: 'varchar', length: 100 }) status: string; - @Column({ type: 'varchar', length: 200 }) - observaciones: string; + @Column({ + type: 'varchar', + length: 200, + nullable: true, // permite NULL + }) + observaciones?: string; @Column({ type: 'bit' }) flag: boolean; - @Column({ type: 'varchar', length: 200 }) - reporte: string; +@Column({ + type: 'varchar', + length: 200, + nullable: true, // permite NULL + }) + reporte?: string; @ManyToOne(() => Usuario, usuario => usuario.movimientos) @JoinColumn({ name: 'id_usuario' }) diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index 7c6d7d0..a4d0980 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -5,30 +5,120 @@ import { UploadedFile, UseInterceptors, BadRequestException, + Request, + Get, + Res, + HttpStatus, } from '@nestjs/common'; + import { FileInterceptor } from '@nestjs/platform-express'; import { ExcelService } from './excel.service'; import { ExcelDocumentation } from './excel.documentation'; +import { MovimientoService } from 'src/movimiento/movimiento.service'; +import { Response } from 'express'; @Controller('excel') export class ExcelController { - constructor(private readonly excelService: ExcelService) {} + constructor( + private readonly excelService: ExcelService, + + private readonly movimientoService: MovimientoService, + ) {} @Post('verify') @UseInterceptors(FileInterceptor('file')) @ExcelDocumentation.verifyExcel() - async verifyExcel(@UploadedFile() file: Express.Multer.File) { + async verifyExcel(@UploadedFile() file: Express.Multer.File, @Request() req) { if (!file) throw new BadRequestException('Se requiere un archivo .xlsx'); + const userId = req.user.userId; + const errors = await this.excelService.validateFile(file.buffer); + + await this.movimientoService.log( + userId, + 'VERIFY', + errors.length ? 'FAILED' : 'SUCCESS', + errors.join('; '), + ); + return { valid: errors.length === 0, errors }; } @Post('load') @UseInterceptors(FileInterceptor('file')) @ExcelDocumentation.loadExcel() - async loadExcel(@UploadedFile() file: Express.Multer.File) { - if (!file) throw new BadRequestException('Se requiere un archivo .xlsx'); - const result = await this.excelService.loadFile(file.buffer); - return result; // { inserted: X } + async loadExcel(@UploadedFile() file: Express.Multer.File, @Request() req) { + const userId = req.user.userId; + try { + const { inserted } = await this.excelService.loadFile(file.buffer); + await this.movimientoService.log( + userId, + 'LOAD', + 'SUCCESS', + undefined, + `inserted=${inserted}`, + ); + return { inserted }; + } catch (err) { + await this.movimientoService.log(userId, 'LOAD', 'FAILED', err.message); + throw err; + } } + + + @ExcelDocumentation.downloadExcel() // <-- decorador correcto + @Get('download') + async downloadData(@Request() req, @Res() res: Response) { + const userId: number = req.user.userId; // asegúrate de que sea primitivo + + try { + // 1) Genera los datos + const data = await this.excelService.generateCsv(); + + // 2) Log SUCCESS + await this.movimientoService.log( + userId, // <-- solo el valor, no userId() + 'DOWNLOAD', + 'SUCCESS', + undefined, + `size=${data.length}`, + ); + + // 3) Devuelve la respuesta + return res + .status(HttpStatus.OK) // <-- HttpStatus importado + .header('Content-Type', 'text/tab-separated-values') + .header('Content-Disposition', 'attachment; filename="usuarios.tsv"') + .send(data); + } catch (err) { + // 4) Log FAILED + await this.movimientoService.log( + userId, + 'DOWNLOAD', + 'FAILED', + err.message, + ); + + // 5) Responde el error + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ statusCode: 500, message: 'Error generando descarga.' }); + } + } + + + + + + + + + + + + + + + + } diff --git a/src/excel/excel.documentation.ts b/src/excel/excel.documentation.ts index 41b5f02..b93d5ca 100644 --- a/src/excel/excel.documentation.ts +++ b/src/excel/excel.documentation.ts @@ -93,4 +93,45 @@ export class ExcelDocumentation { }) ); } + + + + + + + static downloadExcel() { + return applyDecorators( + ApiTags('Excel'), + //ApiBearerAuth('bearer'), + ApiOperation({ + summary: 'Descargar usuarios', + description: 'Exporta todos los usuarios en un archivo TSV (o CSV) y registra el movimiento.' + }), + ApiResponse({ + status: 200, + description: 'TSV generado correctamente', + content: { + 'text/tab-separated-values': { + schema: { type: 'string', example: 'num_cuenta\\tnombre\\t...\\n...' } + } + } + }), + ApiResponse({ status: 401, description: 'No autorizado' }), + ApiResponse({ status: 500, description: 'Error al generar descarga' }), + ); + } + + + + + + + + + + + + + + } diff --git a/src/excel/excel.module.ts b/src/excel/excel.module.ts index c4eec57..2edd062 100644 --- a/src/excel/excel.module.ts +++ b/src/excel/excel.module.ts @@ -3,10 +3,12 @@ import { ExcelService } from './excel.service'; import { ExcelController } from './excel.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Usuario, Genero, Carrera } from '../entities/entities'; +import { MovimientoModule } from 'src/movimiento/movimiento.module'; @Module({ imports: [ TypeOrmModule.forFeature([Usuario, Genero, Carrera]), + MovimientoModule ], providers: [ExcelService], controllers: [ExcelController], diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index 5950085..cde45a9 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -204,4 +204,67 @@ export class ExcelService { return { inserted: rows.length }; } + + + + + + + + + + + + + + + + + + + + + +async generateCsv(): Promise { + 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' + ].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() : '') + + ].join('\t')).join('\n'); + + return header + rows; + } + + + + + + + + + + + + + + + + + } diff --git a/src/movimiento/movimiento.module.ts b/src/movimiento/movimiento.module.ts new file mode 100644 index 0000000..7275540 --- /dev/null +++ b/src/movimiento/movimiento.module.ts @@ -0,0 +1,12 @@ +// src/movimiento/movimiento.module.ts +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { MovimientoService } from './movimiento.service'; +import { Movimiento, Origen } from '../entities/entities'; + +@Module({ + imports: [TypeOrmModule.forFeature([Movimiento, Origen])], + providers: [MovimientoService], + exports: [MovimientoService], +}) +export class MovimientoModule {} diff --git a/src/movimiento/movimiento.service.ts b/src/movimiento/movimiento.service.ts new file mode 100644 index 0000000..a257777 --- /dev/null +++ b/src/movimiento/movimiento.service.ts @@ -0,0 +1,41 @@ +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() +export class MovimientoService { + constructor( + @InjectRepository(Movimiento) + private readonly movRepo: Repository, + @InjectRepository(Origen) + private readonly origenRepo: Repository, + ) {} + + /** Crea un registro de movimiento */ + async log( + usuarioId: number, + fuente: 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}`); + + // 2) Inserta directamente usando los campos de FK + await this.movRepo.insert({ + fecha_mov: new Date(), + status, + 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 }, + }); + } +} From cfa46171ce0ee6b0ddab80871c70ed21f25c7473 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 16 Jun 2025 14:14:23 -0600 Subject: [PATCH 07/22] origen desconocido, falta registrar los origenes y relacionarlos --- src/excel/excel.controller.ts | 89 +++++++++++------------------------ 1 file changed, 28 insertions(+), 61 deletions(-) diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index a4d0980..152593e 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -1,27 +1,19 @@ // src/excel/excel.controller.ts -import { - Controller, - Post, - UploadedFile, - UseInterceptors, - BadRequestException, - Request, - Get, - Res, - HttpStatus, -} from '@nestjs/common'; - +import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiBearerAuth } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express'; -import { ExcelService } from './excel.service'; -import { ExcelDocumentation } from './excel.documentation'; -import { MovimientoService } from 'src/movimiento/movimiento.service'; import { Response } from 'express'; +import { ExcelDocumentation } from './excel.documentation'; +import { ExcelService } from './excel.service'; +import { MovimientoService } from '../movimiento/movimiento.service'; +@UseGuards(AuthGuard('jwt')) +@ApiBearerAuth('bearer') @Controller('excel') export class ExcelController { constructor( private readonly excelService: ExcelService, - private readonly movimientoService: MovimientoService, ) {} @@ -29,18 +21,14 @@ export class ExcelController { @UseInterceptors(FileInterceptor('file')) @ExcelDocumentation.verifyExcel() async verifyExcel(@UploadedFile() file: Express.Multer.File, @Request() req) { - if (!file) throw new BadRequestException('Se requiere un archivo .xlsx'); - const userId = req.user.userId; - + 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('; '), + errors.join('; ') ); - return { valid: errors.length === 0, errors }; } @@ -48,77 +36,56 @@ export class ExcelController { @UseInterceptors(FileInterceptor('file')) @ExcelDocumentation.loadExcel() async loadExcel(@UploadedFile() file: Express.Multer.File, @Request() req) { - const userId = req.user.userId; + const userId: number = req.user.userId; try { - const { inserted } = await this.excelService.loadFile(file.buffer); + const result = await this.excelService.loadFile(file.buffer); await this.movimientoService.log( userId, 'LOAD', 'SUCCESS', undefined, - `inserted=${inserted}`, + `inserted=${result.inserted}` ); - return { inserted }; + return result; } catch (err) { - await this.movimientoService.log(userId, 'LOAD', 'FAILED', err.message); + await this.movimientoService.log( + userId, + 'LOAD', + 'FAILED', + err.message + ); throw err; } } - - @ExcelDocumentation.downloadExcel() // <-- decorador correcto @Get('download') + @ExcelDocumentation.downloadExcel() async downloadData(@Request() req, @Res() res: Response) { - const userId: number = req.user.userId; // asegúrate de que sea primitivo - + const userId: number = req.user.userId; try { - // 1) Genera los datos - const data = await this.excelService.generateCsv(); - - // 2) Log SUCCESS + const csv = await this.excelService.generateCsv(); await this.movimientoService.log( - userId, // <-- solo el valor, no userId() + userId, 'DOWNLOAD', 'SUCCESS', undefined, - `size=${data.length}`, + `size=${csv.length}` ); - - // 3) Devuelve la respuesta return res - .status(HttpStatus.OK) // <-- HttpStatus importado + .status(HttpStatus.OK) .header('Content-Type', 'text/tab-separated-values') .header('Content-Disposition', 'attachment; filename="usuarios.tsv"') - .send(data); + .send(csv); } catch (err) { - // 4) Log FAILED await this.movimientoService.log( userId, 'DOWNLOAD', 'FAILED', - err.message, + err.message ); - - // 5) Responde el error return res .status(HttpStatus.INTERNAL_SERVER_ERROR) .json({ statusCode: 500, message: 'Error generando descarga.' }); } } - - - - - - - - - - - - - - - - } From 5022b1df7e0001874d069ec34dea0ba8bd82fd27 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 16 Jun 2025 14:46:19 -0600 Subject: [PATCH 08/22] insertando origen manualmente y verificando excel endpoints --- infra/init_data.sql | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 infra/init_data.sql diff --git a/infra/init_data.sql b/infra/init_data.sql new file mode 100644 index 0000000..40de51b --- /dev/null +++ b/infra/init_data.sql @@ -0,0 +1,7 @@ +-- init_insert_data.sql +USE carga_test; + +INSERT IGNORE INTO origen (fuente) VALUES + ('redes'), + ('AT'), + ('escolares'); From 8703a292e3e17fdc96b51d35c1dc142b9bc34f32 Mon Sep 17 00:00:00 2001 From: evenegas Date: Tue, 17 Jun 2025 09:19:17 -0600 Subject: [PATCH 09/22] se agregaron cambios en la base de datos --- infra/Dockerfile | 29 +++++++++++++++++++---------- infra/docker-compose.yml | 38 +++++--------------------------------- 2 files changed, 24 insertions(+), 43 deletions(-) diff --git a/infra/Dockerfile b/infra/Dockerfile index e647f20..cd0e134 100644 --- a/infra/Dockerfile +++ b/infra/Dockerfile @@ -1,21 +1,30 @@ -# Usa una imagen de Node.js 22 basada en Alpine +# Usa una imagen base liviana de Node.js FROM node:22.10.0-alpine -# Establece el directorio de trabajo +# Crea un usuario no root para mayor seguridad +RUN adduser -D appuser + +# Establece directorio de trabajo WORKDIR /app -# Instala las dependencias para compilar bcrypt en Alpine -RUN apk add --no-cache python3 make g++ +# Instala herramientas para compilar dependencias nativas +RUN apk add --no-cache python3 make g++ -# Copia los archivos de dependencias y los instala -COPY package.json package-lock.json ./ +# Copia e instala dependencias +COPY package*.json ./ RUN npm install -# Copia el resto del código fuente +# Copia el resto del proyecto COPY . . -# Expone el puerto 3000 +# Da permisos a todo el proyecto a appuser +RUN chown -R appuser:appuser /app + +# Cambia a ese usuario +USER appuser + +# Expone el puerto EXPOSE 3051 -# Comando por defecto para iniciar la aplicación en desarrollo -CMD ["npm", "run", "start:dev"] \ No newline at end of file +# Comando para desarrollo +CMD ["npm", "run", "start:dev"] diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 098ba99..083ccc6 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -1,45 +1,17 @@ +version: '3.9' + services: database: image: mariadb:latest container_name: carga_db restart: always - environment: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: carga_test + env_file: + - ../.env ports: - - "3356:3306" + - "${DB_PORT}:3306" volumes: - mariadb_data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql - - ./init_insert_data.sql:/docker-entrypoint-initdb.d/init_insert_data.sql - - - networks: - - carga_network - - api: - - build: - context: .. - dockerfile: infra/Dockerfile - - container_name: carga_api - - restart: always - depends_on: - - database - env_file: - - ../.env - - ports: - - "3051:3051" - networks: - - carga_network - - volumes: mariadb_data: - -networks: - carga_network: \ No newline at end of file From 4984c47029d1fbb8fa381756fafa7c23bf031e4d Mon Sep 17 00:00:00 2001 From: evenegas Date: Wed, 18 Jun 2025 07:33:21 -0600 Subject: [PATCH 10/22] Se agregaron los campos de los que se deben de mandar a origen de usuario --- .devcontainer/devcontainer.json | 19 ++ src/app.module.ts | 13 +- src/auth/auth.controller.ts | 11 +- src/auth/auth.documentation.ts | 74 +++-- src/auth/auth.module.ts | 6 +- src/auth/auth.service.ts | 40 ++- src/auth/dto/register.dto.ts | 16 +- src/auth/jwt.strategy.ts | 2 +- src/entities/entities.ts | 66 ++--- src/excel/excel.controller.ts | 159 +++++++++-- src/excel/excel.service.ts | 408 ++++++++++++++++++++++----- src/movimiento/movimiento.service.ts | 13 +- 12 files changed, 598 insertions(+), 229 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..af4a840 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 83172c0..ba8aadb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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('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 { } diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index f092d3f..a49b9de 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -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); diff --git a/src/auth/auth.documentation.ts b/src/auth/auth.documentation.ts index b3d5b90..afe30cd 100644 --- a/src/auth/auth.documentation.ts +++ b/src/auth/auth.documentation.ts @@ -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.' }), + ); + } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index d0b3a47..2b406e0 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -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 { } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index b49a1ce..0bacb35 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -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, + @InjectRepository(UsuariosDelSistema) + private readonly userRepo: Repository, + @InjectRepository(Origen) + private readonly origenRepo: Repository, + private readonly jwtService: JwtService, - ) {} + ) { } // 1) Valida email/password - async validateUser(email: string, pass: string): Promise { - const user = await this.userRepo.findOne({ where: { correo: email } }); + async validateUser(email: string, pass: string): Promise { + 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 diff --git a/src/auth/dto/register.dto.ts b/src/auth/dto/register.dto.ts index 3c32c2b..d8bded3 100644 --- a/src/auth/dto/register.dto.ts +++ b/src/auth/dto/register.dto.ts @@ -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; } diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 77658ce..ddf6e23 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -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 }; } } diff --git a/src/entities/entities.ts b/src/entities/entities.ts index a6f1d06..0c71e52 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -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' }) diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index 152593e..bf6eaad 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -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.' }); } + } -} +} \ No newline at end of file diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index cde45a9..56f1574 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -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, // … 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 { @@ -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(); + validateRows(rows: UsuarioRow[]) { + const errors: string[] = []; + const seen = new Set(); - 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 { + // 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 { + 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 { - 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 { + + + + 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 { + + + + 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 { + + + + 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; } diff --git a/src/movimiento/movimiento.service.ts b/src/movimiento/movimiento.service.ts index a257777..45168f6 100644 --- a/src/movimiento/movimiento.service.ts +++ b/src/movimiento/movimiento.service.ts @@ -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, @InjectRepository(Origen) private readonly origenRepo: Repository, - ) {} + ) { } /** 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 }, }); } } From bf83f9d788380d477d2ff4ef5fa3b843347635c5 Mon Sep 17 00:00:00 2001 From: evenegas Date: Wed, 18 Jun 2025 12:54:26 -0600 Subject: [PATCH 11/22] =?UTF-8?q?Se=20agreg=C3=B3=20la=20visualizaci=C3=B3?= =?UTF-8?q?n=20de=20datos=20y=20cambios=20en=20el=20servicio=20de=20excel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 4 +- src/entities/entities.ts | 25 +++---- src/excel/excel.controller.ts | 2 +- src/excel/excel.module.ts | 6 +- src/excel/excel.service.ts | 90 +++++++++++++++----------- src/usuarios/dto/create-usuario.dto.ts | 1 + src/usuarios/dto/update-usuario.dto.ts | 4 ++ src/usuarios/usuarios.controller.ts | 40 ++++++++++++ src/usuarios/usuarios.module.ts | 16 +++++ src/usuarios/usuarios.service.ts | 59 +++++++++++++++++ 10 files changed, 192 insertions(+), 55 deletions(-) create mode 100644 src/usuarios/dto/create-usuario.dto.ts create mode 100644 src/usuarios/dto/update-usuario.dto.ts create mode 100644 src/usuarios/usuarios.controller.ts create mode 100644 src/usuarios/usuarios.module.ts create mode 100644 src/usuarios/usuarios.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index ba8aadb..af3a5de 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,6 +9,7 @@ import * as entities from './entities/entities'; import { ExcelModule } from './excel/excel.module'; import { AuthModule } from './auth/auth.module'; import { MovimientoModule } from './movimiento/movimiento.module'; +import { UsuariosModule } from './usuarios/usuarios.module'; @Module({ @@ -49,7 +50,8 @@ import { MovimientoModule } from './movimiento/movimiento.module'; ExcelModule, AuthModule, - MovimientoModule + MovimientoModule, + UsuariosModule ], controllers: [AppController], diff --git a/src/entities/entities.ts b/src/entities/entities.ts index 0c71e52..45097ae 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -90,6 +90,19 @@ export class Usuario { @OneToMany(() => UsuarioTipoUsuario, utu => utu.usuario) usuarioTipos: UsuarioTipoUsuario[]; } +@Entity({ name: 'carrera_usuario' }) +export class CarreraUsuario { + @PrimaryGeneratedColumn({ name: 'id_carrera_usuario', type: 'int' }) + id_carrera_usuario: number; + + @ManyToOne(() => Carrera, carrera => carrera.carreraUsuarios) + @JoinColumn({ name: 'id_carrera' }) + carrera: Carrera; + + @ManyToOne(() => Usuario, usuario => usuario.carreraUsuarios) + @JoinColumn({ name: 'id_usuario' }) + usuario: Usuario; +} @Entity({ name: 'usuariosDelSistema' }) export class UsuariosDelSistema { @@ -113,19 +126,7 @@ export class UsuariosDelSistema { origen: Origen; } -@Entity({ name: 'carrera_usuario' }) -export class CarreraUsuario { - @PrimaryGeneratedColumn({ name: 'id_carrera_usuario', type: 'int' }) - id_carrera_usuario: number; - @ManyToOne(() => Carrera, carrera => carrera.carreraUsuarios) - @JoinColumn({ name: 'id_carrera' }) - carrera: Carrera; - - @ManyToOne(() => Usuario, usuario => usuario.carreraUsuarios) - @JoinColumn({ name: 'id_usuario' }) - usuario: Usuario; -} @Entity({ name: 'equipo' }) export class Equipo { diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index bf6eaad..1a1fcc5 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -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 e, { Response } from 'express'; +import { Response } from 'express'; import { ExcelDocumentation } from './excel.documentation'; import { ExcelService } from './excel.service'; import { MovimientoService } from '../movimiento/movimiento.service'; diff --git a/src/excel/excel.module.ts b/src/excel/excel.module.ts index 2edd062..4351d3f 100644 --- a/src/excel/excel.module.ts +++ b/src/excel/excel.module.ts @@ -2,15 +2,15 @@ import { Module } from '@nestjs/common'; import { ExcelService } from './excel.service'; import { ExcelController } from './excel.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Usuario, Genero, Carrera } from '../entities/entities'; +import { Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario } from '../entities/entities'; import { MovimientoModule } from 'src/movimiento/movimiento.module'; @Module({ imports: [ - TypeOrmModule.forFeature([Usuario, Genero, Carrera]), + TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario]), MovimientoModule ], providers: [ExcelService], controllers: [ExcelController], }) -export class ExcelModule {} +export class ExcelModule { } diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index 56f1574..04c33f4 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -3,7 +3,7 @@ import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { Workbook } from 'exceljs'; import { In, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; -import { Usuario } from '../entities/entities'; +import { CarreraUsuario, TipoUsuario, Usuario, UsuarioTipoUsuario } from '../entities/entities'; import { Genero } from '../entities/entities'; import { Carrera } from '../entities/entities'; @@ -34,6 +34,12 @@ export class ExcelService { private readonly generoRepo: Repository, @InjectRepository(Carrera) private readonly carreraRepo: Repository, + @InjectRepository(CarreraUsuario) + private readonly carreraUsuarioRepo: Repository, + @InjectRepository(UsuarioTipoUsuario) + private readonly usuarioTipoUsuarioRepo: Repository, + @InjectRepository(TipoUsuario) + private readonly tipoUsuarioRepo: Repository, // … inyecta otros repositorios si los usarás ) { } @@ -181,6 +187,16 @@ export class ExcelService { await this.carreraRepo.save(carrera); } + let tipo = await this.tipoUsuarioRepo.findOne({ + where: { tipo_usuario: r.tipo.trim() }, + }); + if (!tipo) { + tipo = this.tipoUsuarioRepo.create({ + tipo_usuario: r.tipo.trim(), + }); + await this.tipoUsuarioRepo.save(tipo); + } + const generacion = /^[0-9]{4}$/.test(r.gen?.toString().trim()) ? parseInt(r.gen.toString().trim(), 10) : null; @@ -198,12 +214,24 @@ export class ExcelService { }); await this.usuarioRepo.save(usuario); - // 4) Relacion usuario–carrera (tabla intermedia) - await this.carreraRepo - .createQueryBuilder() - .relation('carreraUsuarios') - .of(carrera) - .add(usuario); + + + // 4) Relacion usuario–carrxera (tabla intermedia) + const carreraUsuario = this.carreraUsuarioRepo.create({ + usuario, + carrera, + }); + await this.carreraUsuarioRepo.save(carreraUsuario); + + // 5) Tipo de usuario + + const usuarioTipo = this.usuarioTipoUsuarioRepo.create({ + usuario, + tipoUsuario: tipo, + }); + await this.usuarioTipoUsuarioRepo.save(usuarioTipo); + + } return { inserted: rows.length }; @@ -271,17 +299,7 @@ export class ExcelService { '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, - }, }); @@ -343,17 +361,25 @@ export class ExcelService { '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 usuariosTipo = await this.usuarioRepo.find({ + + relations: [ + 'genero', + 'carreraUsuarios', // relación intermedia + 'carreraUsuarios.carrera', + 'usuarioTipos', // relación con tipo de usuario + 'usuarioTipos.tipoUsuario', // tipo de usuario asociado + ], + + }); + + console.log('Usuarios encontrados:', usuariosTipo); + + console.log('Usuarios encontrados:', usuarios); + @@ -413,10 +439,7 @@ export class ExcelService { 'carreraUsuarios', // relación intermedia 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia ], - select: { - num_cuenta: true, - fecha_nacimiento: true, - }, + }); @@ -466,16 +489,7 @@ export class ExcelService { '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, - }, }); diff --git a/src/usuarios/dto/create-usuario.dto.ts b/src/usuarios/dto/create-usuario.dto.ts new file mode 100644 index 0000000..d7960fe --- /dev/null +++ b/src/usuarios/dto/create-usuario.dto.ts @@ -0,0 +1 @@ +export class CreateUsuarioDto {} diff --git a/src/usuarios/dto/update-usuario.dto.ts b/src/usuarios/dto/update-usuario.dto.ts new file mode 100644 index 0000000..74d19e2 --- /dev/null +++ b/src/usuarios/dto/update-usuario.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateUsuarioDto } from './create-usuario.dto'; + +export class UpdateUsuarioDto extends PartialType(CreateUsuarioDto) {} diff --git a/src/usuarios/usuarios.controller.ts b/src/usuarios/usuarios.controller.ts new file mode 100644 index 0000000..0c80045 --- /dev/null +++ b/src/usuarios/usuarios.controller.ts @@ -0,0 +1,40 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common'; +import { UsuariosService } from './usuarios.service'; +import { CreateUsuarioDto } from './dto/create-usuario.dto'; +import { UpdateUsuarioDto } from './dto/update-usuario.dto'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiBearerAuth } from '@nestjs/swagger'; + + +@Controller('usuarios') +export class UsuariosController { + constructor(private readonly usuariosService: UsuariosService) { } + + + + @Get(':noCuenta') + findOne(@Param('noCuenta') noCuenta: string) { + return this.usuariosService.findOne(noCuenta); + } + // @UseGuards(AuthGuard('jwt')) + // @ApiBearerAuth('bearer') + // @Post() + // create(@Body() noCuenta: string) { + // return this.usuariosService.altaIndividual(createUsuarioDto); + // } + + // @Get() + // findAll() { + // return this.usuariosService.findAll(); + // } + + // @Patch(':id') + // update(@Param('id') id: string, @Body() updateUsuarioDto: UpdateUsuarioDto) { + // return this.usuariosService.update(+id, updateUsuarioDto); + // } + + // @Delete(':id') + // remove(@Param('id') id: string) { + // return this.usuariosService.remove(+id); + // } +} diff --git a/src/usuarios/usuarios.module.ts b/src/usuarios/usuarios.module.ts new file mode 100644 index 0000000..92f81aa --- /dev/null +++ b/src/usuarios/usuarios.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { UsuariosService } from './usuarios.service'; +import { UsuariosController } from './usuarios.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Carrera, CarreraUsuario, Genero, Usuario } from 'src/entities/entities'; +import { MovimientoModule } from 'src/movimiento/movimiento.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario]), + MovimientoModule + ], + controllers: [UsuariosController], + providers: [UsuariosService], +}) +export class UsuariosModule { } diff --git a/src/usuarios/usuarios.service.ts b/src/usuarios/usuarios.service.ts new file mode 100644 index 0000000..37bfb32 --- /dev/null +++ b/src/usuarios/usuarios.service.ts @@ -0,0 +1,59 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CreateUsuarioDto } from './dto/create-usuario.dto'; +import { UpdateUsuarioDto } from './dto/update-usuario.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { CarreraUsuario, Usuario } from 'src/entities/entities'; +import { Repository } from 'typeorm'; + +@Injectable() +export class UsuariosService { + constructor( + @InjectRepository(Usuario) + private readonly usuarioRepository: Repository, + @InjectRepository(CarreraUsuario) + private readonly carreraUsuarioRepository: Repository, + ) { } + + altaIndividual(createUsuarioDto: CreateUsuarioDto) { + return 'This action adds a new usuario'; + } + + async findOne(noCuenta: string) { + const usuario = await this.usuarioRepository.findOne({ + relations: [ + 'genero', + 'carreraUsuarios', + 'carreraUsuarios.carrera', + ], + where: { num_cuenta: noCuenta }, + + }); + + if (!usuario) { + throw new NotFoundException('Usuario no encontrado'); + } + + // Transformar a formato plano + const resultadoPlano = { + num_cuenta: usuario.num_cuenta, + nombre: usuario.nombre, + a_paterno: usuario.a_paterno, + a_materno: usuario.a_materno, + fecha_nacimiento: usuario.fecha_nacimiento, + generacion: usuario.generacion, + genero: usuario.genero?.genero ?? '', + carreras: (usuario.carreraUsuarios ?? []) + .map(cu => cu.carrera?.carrera) + .filter(Boolean) + .join(', '), // todas las carreras como string plano + }; + + return [resultadoPlano]; // importante: frontend espera array + + + } + + update(id: number, updateUsuarioDto: UpdateUsuarioDto) { + return `This action updates a #${id} usuario`; + } +} From 3eafb38f1f72715109229fdecfe8d76647a007ba Mon Sep 17 00:00:00 2001 From: evenegas Date: Fri, 20 Jun 2025 11:59:13 -0600 Subject: [PATCH 12/22] =?UTF-8?q?Se=20agred=C3=B3=20la=20entidad=20servAct?= =?UTF-8?q?ivos,=20se=20agregaron=20funciones=20para=20activacion=20de=20d?= =?UTF-8?q?ervicio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 +- src/auth/auth.documentation.ts | 4 +- src/entities/entities.ts | 89 ++++++---- src/excel/excel.controller.ts | 42 ++++- src/excel/excel.module.ts | 4 +- src/excel/excel.service.ts | 169 +++++++++++++++++-- src/movimiento/movimiento.service.ts | 41 ++++- src/usuarios/entities/servActivos.entitie.ts | 39 +++++ src/usuarios/usuarios.controller.ts | 2 +- src/usuarios/usuarios.module.ts | 5 +- src/usuarios/usuarios.service.ts | 76 ++++++--- 11 files changed, 388 insertions(+), 85 deletions(-) create mode 100644 src/usuarios/entities/servActivos.entitie.ts diff --git a/src/app.module.ts b/src/app.module.ts index af3a5de..20b4306 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -41,7 +41,7 @@ import { UsuariosModule } from './usuarios/usuarios.module'; logger: 'advanced-console', retryAttempts: 5, retryDelay: 2000, - dropSchema: true, // solo para desarrollo + dropSchema: false, // solo para desarrollo }; }, }), diff --git a/src/auth/auth.documentation.ts b/src/auth/auth.documentation.ts index afe30cd..537a7f6 100644 --- a/src/auth/auth.documentation.ts +++ b/src/auth/auth.documentation.ts @@ -77,7 +77,7 @@ export class AuthDocumentation { properties: { email: { type: 'string', format: 'email', example: 'nuevo@dominio.com' }, password: { type: 'string', example: 'Password123' }, - origen: { type: 'string', example: 'redes', description: 'Origen del usuario' } + origen: { type: 'string', example: 'RED', description: 'Origen del usuario' } } } @@ -91,7 +91,7 @@ export class AuthDocumentation { id_usuario: { type: 'number', example: 1 }, correo: { type: 'string', example: 'nuevo@dominio.com' }, - origen: { type: 'string', example: 'redes' } + origen: { type: 'string', example: 'RED' } } diff --git a/src/entities/entities.ts b/src/entities/entities.ts index 45097ae..27d6c61 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -1,4 +1,5 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn, PrimaryColumn } from 'typeorm'; +import { ServActivos } from 'src/usuarios/entities/servActivos.entitie'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn, PrimaryColumn, OneToOne } from 'typeorm'; @Entity({ name: 'origen' }) export class Origen { @@ -42,6 +43,44 @@ export class Carrera { carreraUsuarios: CarreraUsuario[]; } +@Entity({ name: 'movimiento' }) +export class Movimiento { + @PrimaryGeneratedColumn({ name: 'id_mov', type: 'int' }) + id_mov: number; + + @Column({ name: 'fecha_mov', type: 'datetime' }) + fecha_mov: Date; + + @Column({ type: 'varchar', length: 100 }) + status: string; + + @Column({ + type: 'varchar', + length: 200, + nullable: true, // permite NULL + }) + observaciones?: string; + + @Column({ type: 'bit' }) + flag: boolean; + + @Column({ + type: 'varchar', + length: 200, + nullable: true, // permite NULL + }) + reporte?: string; + + @OneToMany(() => Usuario, usuario => usuario.movimiento) + usuario: Usuario[]; + + + @ManyToOne(() => Origen, origen => origen.movimientos) + @JoinColumn({ name: 'id_origen' }) + origen: Origen; +} + + @Entity({ name: 'usuario' }) export class Usuario { @PrimaryGeneratedColumn({ name: 'id_usuario', type: 'int' }) @@ -89,7 +128,17 @@ export class Usuario { @OneToMany(() => UsuarioTipoUsuario, utu => utu.usuario) usuarioTipos: UsuarioTipoUsuario[]; + + @ManyToOne(() => Movimiento, movimiento => movimiento.usuario) + @JoinColumn({ name: 'id_movimiento' }) + movimiento: Movimiento; + + @OneToOne(() => ServActivos, (servActivo) => servActivo.usuario,) + servActivo: ServActivos; } + + + @Entity({ name: 'carrera_usuario' }) export class CarreraUsuario { @PrimaryGeneratedColumn({ name: 'id_carrera_usuario', type: 'int' }) @@ -152,6 +201,12 @@ export class TipoUsuario { usuariosTipos: UsuarioTipoUsuario[]; } + + + + + + @Entity({ name: 'usuario_tipo_usuario' }) export class UsuarioTipoUsuario { @PrimaryGeneratedColumn({ name: 'id_user_tipo_usuario', type: 'int' }) @@ -166,36 +221,6 @@ export class UsuarioTipoUsuario { usuario: Usuario; } -@Entity({ name: 'movimiento' }) -export class Movimiento { - @PrimaryGeneratedColumn({ name: 'id_mov', type: 'int' }) - id_mov: number; - @Column({ name: 'fecha_mov', type: 'datetime' }) - fecha_mov: Date; +export { ServActivos }; - @Column({ type: 'varchar', length: 100 }) - status: string; - - @Column({ - type: 'varchar', - length: 200, - nullable: true, // permite NULL - }) - observaciones?: string; - - @Column({ type: 'bit' }) - flag: boolean; - - @Column({ - type: 'varchar', - length: 200, - nullable: true, // permite NULL - }) - reporte?: string; - - - @ManyToOne(() => Origen, origen => origen.movimientos) - @JoinColumn({ name: 'id_origen' }) - origen: Origen; -} diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index 1a1fcc5..c1ad4ae 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -1,5 +1,5 @@ // src/excel/excel.controller.ts -import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus } from '@nestjs/common'; +import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus, Param, ParseIntPipe } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express'; @@ -22,7 +22,8 @@ export class ExcelController { @ExcelDocumentation.verifyExcel() async verifyExcel(@UploadedFile() file: Express.Multer.File, @Request() req) { const userId: number = req.user.userId; // ahora sí existe - const errors = await this.excelService.validateFile(file.buffer); + const status = await this.excelService.validateFile(file.buffer); + const errors = status.errors; await this.movimientoService.log( 'VERIFY', @@ -47,14 +48,17 @@ export class ExcelController { } try { const result = await this.excelService.loadFile(file.buffer); - await this.movimientoService.log( - - 'LOAD', + //Actualizar el estado de los movimientos + if (!result) { + throw new Error('No se pudo procesar el archivo.'); + } + await this.movimientoService.updateStatus( + result.id_movimiento, 'SUCCESS', - undefined, `inserted=${result.inserted}` ); - return result; + return result + } catch (err) { await this.movimientoService.log( @@ -189,4 +193,28 @@ export class ExcelController { } } + + //Agregarlo en su propio controller + @Get('movimientos') + async getMovimientos() { + const movimientos = await this.movimientoService.findAll(); + return movimientos; + } + + @Post('activar-servicios/:id_movimiento') + async activarServicios( + @Param('id_movimiento', ParseIntPipe) id_movimiento: number, + @Request() req, + ): Promise<{ message: string }> { + const origen = req.user.origen; + + if (!origen) { + throw new Error('No se encontró origen en el JWT'); + } + + await this.excelService.activarServicios(id_movimiento, origen); + + return { message: 'Servicios activados correctamente' }; + } + } \ No newline at end of file diff --git a/src/excel/excel.module.ts b/src/excel/excel.module.ts index 4351d3f..984a73b 100644 --- a/src/excel/excel.module.ts +++ b/src/excel/excel.module.ts @@ -4,10 +4,12 @@ import { ExcelController } from './excel.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario } from '../entities/entities'; import { MovimientoModule } from 'src/movimiento/movimiento.module'; +import { ServActivos } from 'src/usuarios/entities/servActivos.entitie'; + @Module({ imports: [ - TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario]), + TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario, ServActivos]), MovimientoModule ], providers: [ExcelService], diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index 04c33f4..189e1e5 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -6,6 +6,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { CarreraUsuario, TipoUsuario, Usuario, UsuarioTipoUsuario } from '../entities/entities'; import { Genero } from '../entities/entities'; import { Carrera } from '../entities/entities'; +import { MovimientoService } from 'src/movimiento/movimiento.service'; +import { ServActivos } from 'src/usuarios/entities/servActivos.entitie'; export interface UsuarioRow { cuenta: string; @@ -40,6 +42,10 @@ export class ExcelService { private readonly usuarioTipoUsuarioRepo: Repository, @InjectRepository(TipoUsuario) private readonly tipoUsuarioRepo: Repository, + @InjectRepository(ServActivos) + private readonly servActivosRepo: Repository, + private readonly movimientoService: MovimientoService, + // … inyecta otros repositorios si los usarás ) { } @@ -101,23 +107,35 @@ export class ExcelService { validateRows(rows: UsuarioRow[]) { const errors: string[] = []; const seen = new Set(); + const rowsGood: UsuarioRow[] = []; + let flagError: boolean = false; rows.forEach((r, i) => { const rowNum = i + 2; // por el encabezado // Campos que no pueden quedar vacíos - ['cuenta', 'nombreCompleto'].forEach(field => { + ['cuenta', 'nombreCompleto', 'tipo'].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.`); + flagError = true; } }); + // El número de cuenta debe ser de nueve dígitos para los alumnos + if (r.cuenta && !/^\d{9}$/.test(r.cuenta.toString().trim()) && r.tipo.trim() === 'Licenciatura') { + errors.push(`Fila ${rowNum}: cuenta "${r.cuenta}" debe tener 9 dígitos.`); + flagError = true; + } + + + // Duplicados de "cuenta" const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : ''; if (seen.has(cuentaStr)) { errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`); + flagError = true; } else { seen.add(cuentaStr); } @@ -128,6 +146,7 @@ export class ExcelService { 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}".`); + flagError = true; } @@ -135,16 +154,29 @@ export class ExcelService { 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}".`); + flagError = true; } // 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.`); + flagError = true; } + + if (!flagError) { + rowsGood.push(r) + } else { + flagError = false; // reset para la siguiente fila + } + + + }); - return errors; + console.log(rowsGood); + + return { errors, rowsGood }; } @@ -153,21 +185,42 @@ export class ExcelService { */ async validateFile(buffer: Buffer) { const rows = await this.parseFile(buffer); - const errs = this.validateRows(rows); - return errs; + const status = this.validateRows(rows); + return status; } /** * Valida y luego persiste usuarios y relaciones básicas */ async loadFile(buffer: Buffer) { - const rows = await this.parseFile(buffer); - const errs = this.validateRows(rows); - if (errs.length) { - throw new BadRequestException({ errors: errs }); - } + const file = await this.parseFile(buffer); + const status = this.validateRows(file); + const errs = status.errors; + const rows = status.rowsGood; + + const movimiento = await this.movimientoService.log( + + 'LOAD', + 'LOADING', + undefined + ); + + + + for (const r of rows) { + //0.1 + const userExists = await this.usuarioRepo.findOne({ + where: { num_cuenta: r.cuenta }, + }); + + if (userExists) { + errs.push(`Cuenta ${r.cuenta} ya existe, se omite.`); + continue; // si ya existe, no lo insertamos + } + + // 1) Género (crea o reutiliza) let genero = await this.generoRepo.findOne({ where: { genero: r.sexo } }); if (!genero) { @@ -200,6 +253,8 @@ export class ExcelService { 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, @@ -210,9 +265,10 @@ export class ExcelService { fecha_nacimiento: r.fechnac, generacion: generacion, genero, + movimiento: { id_mov: movimiento.id_mov }, // Relación con movimiento }); - await this.usuarioRepo.save(usuario); + const user = await this.usuarioRepo.save(usuario); @@ -231,14 +287,50 @@ export class ExcelService { }); await this.usuarioTipoUsuarioRepo.save(usuarioTipo); + // 6) ServActivos (si es necesario) + switch (r.tipo.trim()) { + + + case 'Licenciatura': + case 'Profesor': + const servActivos = this.servActivosRepo.create({ + + Red: true, + AT: true, + Correo: true, + Prestamos: true, + usuario: user, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }); + await this.servActivosRepo.save(servActivos); + + break; + + + + case 'Trabajadores': + const servActivosTrabajadores = this.servActivosRepo.create({ + Red: true, + usuario: user, + RedStatus: 'Inactivo', + + }); + await this.servActivosRepo.save(servActivosTrabajadores); + break; + } + } - - return { inserted: rows.length }; + return { inserted: rows.length, id_movimiento: movimiento.id_mov, errors: errs }; } + + // async generateCsv(): Promise { // const users = await this.usuarioRepo.find({ // relations: ['genero', 'carreraUsuarios'], // si quieres más info @@ -293,6 +385,9 @@ export class ExcelService { tipo_usuario: In(tipos), }, }, + servActivo: { + CorreoStatus: 'Inactivo', // solo los que no tienen servicio activo + } }, relations: [ 'genero', @@ -355,6 +450,9 @@ export class ExcelService { tipo_usuario: In(tipos), }, }, + servActivo: { + PrestamosStatus: 'Inactivo', // solo los que no tienen servicio activo + } }, relations: [ 'genero', @@ -433,6 +531,9 @@ export class ExcelService { tipo_usuario: In(tipos), }, }, + servActivo: { + RedStatus: 'Inactivo', + } }, relations: [ 'genero', @@ -483,6 +584,9 @@ export class ExcelService { tipo_usuario: In(tipos), }, }, + servActivo: { + ATStatus: 'Inactivo', + }, }, relations: [ 'genero', @@ -523,17 +627,52 @@ export class ExcelService { + //Funcion que se le debe hacer su propio módulo + async activarServicios(id_movimiento: number, origen: string): Promise { + if (!['AT', 'SOLICITA', 'CORREO', 'RED'].includes(origen)) { + throw new BadRequestException(`Origen ${origen} no válido`); + } + const usuarios = await this.usuarioRepo.find({ + where: { movimiento: { id_mov: id_movimiento } }, + }); + if (!usuarios || usuarios.length === 0) { + throw new BadRequestException('No se encontró el usuario asociado al movimiento'); + } + for (const usuario of usuarios) { + const servActivos = await this.servActivosRepo.findOne({ + where: { usuario: { id_usuario: usuario.id_usuario } }, + }); + if (!servActivos) { + throw new BadRequestException( + `No se encontraron servicios activos para el usuario con ID ${usuario.id_usuario}`, + ); + } + const timestamp = 'Activo ' + new Date().toISOString(); + if (origen === 'AT' && servActivos.AT) { + servActivos.ATStatus = timestamp; + } else if (origen === 'RED' && servActivos.Red) { + servActivos.RedStatus = timestamp; + } else if (origen === 'CORREO' && servActivos.Correo) { + servActivos.CorreoStatus = timestamp; + } else if (origen === 'SOLICITA' && servActivos.Prestamos) { + servActivos.PrestamosStatus = timestamp; + } else { + throw new BadRequestException( + `Origen ${origen} no válido o servicio no activo para el usuario con ID ${usuario.id_usuario}`, + ); + } + await this.servActivosRepo.save(servActivos); + console.log(`Servicio ${servActivos}`); + } - - - + } diff --git a/src/movimiento/movimiento.service.ts b/src/movimiento/movimiento.service.ts index 45168f6..f91e5fd 100644 --- a/src/movimiento/movimiento.service.ts +++ b/src/movimiento/movimiento.service.ts @@ -20,19 +20,50 @@ export class MovimientoService { observaciones?: string, reporte?: string, flag = false, - ) { + ): Promise { // 1) Obtén el origen o falla si no existe 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({ + // 2) Crea el objeto movimiento + const movimiento = this.movRepo.create({ fecha_mov: new Date(), status, - observaciones, // aquí usas el valor que vino como parámetro - reporte, // idem + observaciones, + reporte, flag, origen: { id_origen: origin.id_origen }, }); + + // 3) Guarda y devuelve el ID + const saved = await this.movRepo.save(movimiento); + return saved; } + + + /** Actualiza el status de un movimiento existente */ + async updateStatus(id_movimiento: number, nuevoStatus: string, observaciones?: string): Promise { + const movimiento = await this.movRepo.findOne({ where: { id_mov: id_movimiento } }); + + if (!movimiento) { + throw new Error(`Movimiento con ID ${id_movimiento} no encontrado`); + } + + movimiento.status = nuevoStatus; + movimiento.observaciones = observaciones || movimiento.observaciones; // opcional: actualizar observaciones + movimiento.fecha_mov = new Date(); // opcional: actualizar fecha + + await this.movRepo.save(movimiento); + } + + async findAll(): Promise { + return await this.movRepo.find({ where: { origen: { origen: 'LOAD' } } }); + } + + + + + + + } diff --git a/src/usuarios/entities/servActivos.entitie.ts b/src/usuarios/entities/servActivos.entitie.ts new file mode 100644 index 0000000..d3980ba --- /dev/null +++ b/src/usuarios/entities/servActivos.entitie.ts @@ -0,0 +1,39 @@ +import { Usuario } from "src/entities/entities"; +import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from "typeorm"; + +@Entity({ name: 'servActivos' }) +export class ServActivos { + @PrimaryGeneratedColumn({ name: 'id_servActivos' }) + id_servActivos: number; + + @OneToOne(() => Usuario, (usuario) => usuario.servActivo, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_usuario' }) + usuario: Usuario; + + + @Column({ type: 'boolean', default: false }) + Red: boolean; + + @Column({ type: 'boolean', default: false }) + Prestamos: boolean; + + @Column({ type: 'boolean', default: false }) + AT: boolean; + + @Column({ type: 'boolean', default: false }) + Correo: boolean; + + @Column({ type: 'varchar', length: 40, nullable: true }) + RedStatus: string; + + @Column({ type: 'varchar', length: 40, nullable: true }) + PrestamosStatus: string; + + @Column({ type: 'varchar', length: 40, nullable: true }) + ATStatus: string; + + @Column({ type: 'varchar', length: 40, nullable: true }) + CorreoStatus: string; + + +} diff --git a/src/usuarios/usuarios.controller.ts b/src/usuarios/usuarios.controller.ts index 0c80045..a0d1e65 100644 --- a/src/usuarios/usuarios.controller.ts +++ b/src/usuarios/usuarios.controller.ts @@ -14,7 +14,7 @@ export class UsuariosController { @Get(':noCuenta') findOne(@Param('noCuenta') noCuenta: string) { - return this.usuariosService.findOne(noCuenta); + return this.usuariosService.findOneStatus(noCuenta); } // @UseGuards(AuthGuard('jwt')) // @ApiBearerAuth('bearer') diff --git a/src/usuarios/usuarios.module.ts b/src/usuarios/usuarios.module.ts index 92f81aa..1741048 100644 --- a/src/usuarios/usuarios.module.ts +++ b/src/usuarios/usuarios.module.ts @@ -4,13 +4,16 @@ import { UsuariosController } from './usuarios.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Carrera, CarreraUsuario, Genero, Usuario } from 'src/entities/entities'; import { MovimientoModule } from 'src/movimiento/movimiento.module'; +import { ServActivos } from './entities/servActivos.entitie'; @Module({ imports: [ - TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario]), + TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario, ServActivos]), MovimientoModule + ], controllers: [UsuariosController], providers: [UsuariosService], + exports: [TypeOrmModule.forFeature([ServActivos])], }) export class UsuariosModule { } diff --git a/src/usuarios/usuarios.service.ts b/src/usuarios/usuarios.service.ts index 37bfb32..251251c 100644 --- a/src/usuarios/usuarios.service.ts +++ b/src/usuarios/usuarios.service.ts @@ -4,6 +4,7 @@ import { UpdateUsuarioDto } from './dto/update-usuario.dto'; import { InjectRepository } from '@nestjs/typeorm'; import { CarreraUsuario, Usuario } from 'src/entities/entities'; import { Repository } from 'typeorm'; +import { ServActivos } from './entities/servActivos.entitie'; @Injectable() export class UsuariosService { @@ -12,47 +13,82 @@ export class UsuariosService { private readonly usuarioRepository: Repository, @InjectRepository(CarreraUsuario) private readonly carreraUsuarioRepository: Repository, + @InjectRepository(ServActivos) + private readonly servActivosRepository: Repository, ) { } altaIndividual(createUsuarioDto: CreateUsuarioDto) { return 'This action adds a new usuario'; } - async findOne(noCuenta: string) { + async findOneStatus(noCuenta: string) { const usuario = await this.usuarioRepository.findOne({ - relations: [ - 'genero', - 'carreraUsuarios', - 'carreraUsuarios.carrera', - ], where: { num_cuenta: noCuenta }, - + select: ['id_usuario'], }); + + + + if (!usuario) { throw new NotFoundException('Usuario no encontrado'); } + const servActivo = await this.servActivosRepository.findOne({ + where: { usuario: { id_usuario: usuario.id_usuario } }, + + }); + if (!servActivo) { + throw new NotFoundException('Servicios no encontrados para el usuario'); + } + + + // Transformar a formato plano - const resultadoPlano = { - num_cuenta: usuario.num_cuenta, - nombre: usuario.nombre, - a_paterno: usuario.a_paterno, - a_materno: usuario.a_materno, - fecha_nacimiento: usuario.fecha_nacimiento, - generacion: usuario.generacion, - genero: usuario.genero?.genero ?? '', - carreras: (usuario.carreraUsuarios ?? []) - .map(cu => cu.carrera?.carrera) - .filter(Boolean) - .join(', '), // todas las carreras como string plano - }; + let resultadoPlano = {}; + + if (servActivo.AT) { + resultadoPlano = { + AT: servActivo.ATStatus + } + } + if (servActivo.Red) { + resultadoPlano = { + ...resultadoPlano, + Red: servActivo.RedStatus + } + } + if (servActivo.Prestamos) { + resultadoPlano = { + ...resultadoPlano, + Prestamos: servActivo.PrestamosStatus + } + } + if (servActivo.Correo) { + resultadoPlano = { + ...resultadoPlano, + Correo: servActivo.CorreoStatus + } + } + + // Si no hay servicios activos, retornar un objeto vacío + if (Object.keys(resultadoPlano).length === 0) { + resultadoPlano = { + Servicios: 'No hay servicios activos para este usuario' + }; + } + console.log('Resultado plano:', resultadoPlano); return [resultadoPlano]; // importante: frontend espera array + + } + + update(id: number, updateUsuarioDto: UpdateUsuarioDto) { return `This action updates a #${id} usuario`; } From a7ab20be29a9d431c744ec37659d861670a7e278 Mon Sep 17 00:00:00 2001 From: evenegas Date: Mon, 30 Jun 2025 12:53:08 -0600 Subject: [PATCH 13/22] Se agregaron validaciones --- .env(Example) | 18 + package-lock.json | 52 +++ package.json | 2 + src/app.module.ts | 11 +- src/entities/entities.ts | 1 - src/excel/dto/cargaIndividual.dto.ts | 0 src/excel/excel.controller.ts | 84 +++- src/excel/excel.module.ts | 12 +- src/excel/excel.service.ts | 135 ++++++- src/mail/dto/send-email.dto.ts | 25 ++ src/mail/mail.controller.ts | 31 ++ src/mail/mail.module.ts | 12 + src/mail/mail.service.ts | 76 ++++ src/movimiento/movimiento.module.ts | 6 +- src/movimiento/movimiento.service.ts | 191 +++++++++- src/usuarios/dto/create-usuario.dto.ts | 47 ++- src/usuarios/usuarios.controller.ts | 7 + src/usuarios/usuarios.module.ts | 8 +- src/usuarios/usuarios.service.ts | 506 ++++++++++++++++++++++--- 19 files changed, 1149 insertions(+), 75 deletions(-) create mode 100644 .env(Example) create mode 100644 src/excel/dto/cargaIndividual.dto.ts create mode 100644 src/mail/dto/send-email.dto.ts create mode 100644 src/mail/mail.controller.ts create mode 100644 src/mail/mail.module.ts create mode 100644 src/mail/mail.service.ts diff --git a/.env(Example) b/.env(Example) new file mode 100644 index 0000000..3db2053 --- /dev/null +++ b/.env(Example) @@ -0,0 +1,18 @@ +API_PORT = 4000 +JWT_SECRET = "your_jwt_secret" +JWT_EXPIRES_IN = "1h" +DB_HOST = localhost +DB_PORT = 32769 +DB_USER = root +DB_PASS = root +DB_NAME = carga_test +DB_SYNC = true +//api +API_URL = //URL web service + + + //Gmail configuration, llaves de acceso + EMAIL_USER = + CLIENT_ID = + CLIENT_SECRET = + REFRESH_TOKEN = \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5818d64..68cf1dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.1.3", + "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", "bcrypt": "^6.0.0", @@ -24,6 +25,7 @@ "exceljs": "^4.4.0", "multer": "^2.0.1", "mysql2": "^3.14.1", + "nodemailer": "^7.0.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", @@ -2657,6 +2659,19 @@ "@nestjs/core": "^11.0.0" } }, + "node_modules/@nestjs/schedule": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.0.tgz", + "integrity": "sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==", + "license": "MIT", + "dependencies": { + "cron": "4.3.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/schematics": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.5.tgz", @@ -3566,6 +3581,12 @@ "@types/node": "*" } }, + "node_modules/@types/luxon": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", + "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==", + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -6158,6 +6179,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/cron": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz", + "integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.6.0", + "luxon": "~3.6.0" + }, + "engines": { + "node": ">=18.x" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -9504,6 +9538,15 @@ "url": "https://github.com/sponsors/wellwelwel" } }, + "node_modules/luxon": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -9935,6 +9978,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", + "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index 77685cf..69e6dc3 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.1.3", + "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", "bcrypt": "^6.0.0", @@ -35,6 +36,7 @@ "exceljs": "^4.4.0", "multer": "^2.0.1", "mysql2": "^3.14.1", + "nodemailer": "^7.0.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", diff --git a/src/app.module.ts b/src/app.module.ts index 20b4306..a98f4f9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,10 +10,14 @@ import { ExcelModule } from './excel/excel.module'; import { AuthModule } from './auth/auth.module'; import { MovimientoModule } from './movimiento/movimiento.module'; import { UsuariosModule } from './usuarios/usuarios.module'; +import { MailService } from './mail/mail.service'; +import { MailModule } from './mail/mail.module'; +import { ScheduleModule } from '@nestjs/schedule'; @Module({ imports: [ + ScheduleModule.forRoot(), ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRootAsync({ imports: [ConfigModule], @@ -27,6 +31,7 @@ import { UsuariosModule } from './usuarios/usuarios.module'; // no prints en claro en prod: password: config.get('DB_PASS', ''), database: config.get('DB_NAME', 'test'), + dropSchema: false, // solo para desarrollo }; Logger.log('[DB CONFIG] ' + JSON.stringify({ ...dbConfig, @@ -39,9 +44,10 @@ import { UsuariosModule } from './usuarios/usuarios.module'; logging: ['error', 'warn', 'info', 'query', 'schema'], logger: 'advanced-console', + retryAttempts: 5, retryDelay: 2000, - dropSchema: false, // solo para desarrollo + }; }, }), @@ -51,7 +57,8 @@ import { UsuariosModule } from './usuarios/usuarios.module'; ExcelModule, AuthModule, MovimientoModule, - UsuariosModule + UsuariosModule, + MailModule ], controllers: [AppController], diff --git a/src/entities/entities.ts b/src/entities/entities.ts index 27d6c61..5a0ee55 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -88,7 +88,6 @@ export class Usuario { @Column({ type: 'varchar', - length: 9, unique: true, nullable: true, default: null, diff --git a/src/excel/dto/cargaIndividual.dto.ts b/src/excel/dto/cargaIndividual.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index c1ad4ae..d8bc7af 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -1,5 +1,5 @@ // src/excel/excel.controller.ts -import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus, Param, ParseIntPipe } from '@nestjs/common'; +import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus, Param, ParseIntPipe, Body, BadRequestException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express'; @@ -7,6 +7,10 @@ import { Response } from 'express'; import { ExcelDocumentation } from './excel.documentation'; import { ExcelService } from './excel.service'; import { MovimientoService } from '../movimiento/movimiento.service'; +import { MailService } from 'src/mail/mail.service'; +import { CreateUsuarioDto } from 'src/usuarios/dto/create-usuario.dto'; +import { Origen } from 'src/entities/entities'; +import { UsuariosService } from 'src/usuarios/usuarios.service'; @UseGuards(AuthGuard('jwt')) @ApiBearerAuth('bearer') @@ -15,8 +19,35 @@ export class ExcelController { constructor( private readonly excelService: ExcelService, private readonly movimientoService: MovimientoService, + private readonly mailService: MailService, + private readonly usuarioService: UsuariosService // Asegúrate de importar el servicio de correo ) { } + + @Post('carga') + async cargaUsuario(@Request() req, @Body() usuario: CreateUsuarioDto) { + console.log('Usuario recibido:', usuario); + if (req.user.origen != 'LOAD') { + throw new BadRequestException('Origen no permitido para carga') + } + const alta = await this.usuarioService.cargaIndividual(usuario, req.user.origen) + console.log('Alta de usuario:', alta); + await this.movimientoService.updateStatus(alta.movId, 'SUCCESS') + + this.mailService.sendMail({ + to: req.user.email, // Asegúrate de que el usuario tenga un email + subject: 'Carga de datos exitosa', + text: "La carga de datos se ha realizado con éxito.", + html: `

La carga de datos se ha realizado con éxito. ` + alta, + }); + + await this.excelService.enviarCargaMasiva(); + + + + } + + @Post('verify') @UseInterceptors(FileInterceptor('file')) @ExcelDocumentation.verifyExcel() @@ -40,7 +71,7 @@ export class ExcelController { const origen = req.user.origen; // ahora sí existe if (origen != 'LOAD') { await this.movimientoService.log( - 'LOAD', + origen, 'FAILED', 'Origen no permitido para carga' ); @@ -50,13 +81,46 @@ export class ExcelController { const result = await this.excelService.loadFile(file.buffer); //Actualizar el estado de los movimientos if (!result) { + this.mailService.sendMail({ + to: req.user.email, // Asegúrate de que el usuario tenga un email + subject: 'Carga de datos Fallida', + text: "No se pudieron subir datos.", + html: '

No se pudieron subir datos.

', + }); throw new Error('No se pudo procesar el archivo.'); } + + if (result.inserted == 0) { + this.mailService.sendMail({ + to: req.user.email, // Asegúrate de que el usuario tenga un email + subject: 'Carga de datos Fallida', + text: "No se pudieron subir datos.", + html: `

Se han insertado ${result.inserted} registros.

+

Errores encontrados:

+
    ${result.errors.map(error => `
  • ${error}
  • `).join('')}
`, + }); + + return result + } + await this.movimientoService.updateStatus( result.id_movimiento, 'SUCCESS', `inserted=${result.inserted}` ); + + this.mailService.sendMail({ + to: req.user.email, // Asegúrate de que el usuario tenga un email + subject: 'Carga de datos exitosa', + text: "La carga de datos se ha realizado con éxito.", + html: `

La carga de datos se ha realizado con éxito. Se han insertado ${result.inserted} registros.

+

Errores encontrados:

+
    ${result.errors.map(error => `
  • ${error}
  • `).join('')}
`, + }); + + await this.excelService.enviarCargaMasiva(); + + return result } catch (err) { @@ -196,8 +260,9 @@ export class ExcelController { //Agregarlo en su propio controller @Get('movimientos') - async getMovimientos() { - const movimientos = await this.movimientoService.findAll(); + async getMovimientos(@Request() req) { + + const movimientos = await this.movimientoService.findAll(req.user.origen); return movimientos; } @@ -217,4 +282,15 @@ export class ExcelController { return { message: 'Servicios activados correctamente' }; } + + @Get('cargaIndividual') + async cargarIndividual( + @Request() req) { + const origen = req.user.origen; // ahora sí existe + return this.movimientoService.findAllCargaIndv(origen); + + } + + + } \ No newline at end of file diff --git a/src/excel/excel.module.ts b/src/excel/excel.module.ts index 984a73b..8fcef3b 100644 --- a/src/excel/excel.module.ts +++ b/src/excel/excel.module.ts @@ -2,17 +2,21 @@ import { Module } from '@nestjs/common'; import { ExcelService } from './excel.service'; import { ExcelController } from './excel.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario } from '../entities/entities'; +import { Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario, UsuariosDelSistema } from '../entities/entities'; import { MovimientoModule } from 'src/movimiento/movimiento.module'; import { ServActivos } from 'src/usuarios/entities/servActivos.entitie'; +import { MailModule } from 'src/mail/mail.module'; +import { UsuariosService } from 'src/usuarios/usuarios.service'; @Module({ imports: [ - TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario, ServActivos]), - MovimientoModule + TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario, UsuarioTipoUsuario, TipoUsuario, ServActivos, UsuariosDelSistema]), + MovimientoModule, + MailModule, + ], - providers: [ExcelService], + providers: [ExcelService, UsuariosService], controllers: [ExcelController], }) export class ExcelModule { } diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index 189e1e5..1a36a1b 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -3,11 +3,12 @@ import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { Workbook } from 'exceljs'; import { In, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; -import { CarreraUsuario, TipoUsuario, Usuario, UsuarioTipoUsuario } from '../entities/entities'; +import { CarreraUsuario, TipoUsuario, Usuario, UsuariosDelSistema, UsuarioTipoUsuario } from '../entities/entities'; import { Genero } from '../entities/entities'; import { Carrera } from '../entities/entities'; import { MovimientoService } from 'src/movimiento/movimiento.service'; import { ServActivos } from 'src/usuarios/entities/servActivos.entitie'; +import { MailService } from 'src/mail/mail.service'; export interface UsuarioRow { cuenta: string; @@ -44,7 +45,10 @@ export class ExcelService { private readonly tipoUsuarioRepo: Repository, @InjectRepository(ServActivos) private readonly servActivosRepo: Repository, + @InjectRepository(UsuariosDelSistema) + private readonly usuariosDelSistemaRepo: Repository, private readonly movimientoService: MovimientoService, + private readonly mailService: MailService // … inyecta otros repositorios si los usarás ) { } @@ -197,17 +201,24 @@ export class ExcelService { const status = this.validateRows(file); const errs = status.errors; const rows = status.rowsGood; + let carga + if (rows.length == 0) { + throw new BadRequestException('No se encontraron filas válidas para cargar. ' + errs); + } + const movimiento = await this.movimientoService.log( 'LOAD', 'LOADING', - undefined + undefined, + `CARGA MASIVA DE USUARIOS`, + true, // flag para indicar que es una carga masiva ); - + let contador: number = 0; for (const r of rows) { //0.1 @@ -217,6 +228,7 @@ export class ExcelService { if (userExists) { errs.push(`Cuenta ${r.cuenta} ya existe, se omite.`); + contador++; continue; // si ya existe, no lo insertamos } @@ -290,7 +302,71 @@ export class ExcelService { // 6) ServActivos (si es necesario) switch (r.tipo.trim()) { + case 'Diplomado': + const servActivosDiplomado = this.servActivosRepo.create({ + Correo: true, + usuario: user, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }); + await this.servActivosRepo.save(servActivosDiplomado); + break; + + case 'Extra Largo': + const servActivosExtraLargo = this.servActivosRepo.create({ + Prestamos: true, + Correo: true, + usuario: user, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }); + await this.servActivosRepo.save(servActivosExtraLargo); + break; + + case 'Servicio Social': + const servActivosServicio = this.servActivosRepo.create({ + Red: true, + AT: true, + Correo: true, + usuario: user, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }); + await this.servActivosRepo.save(servActivosServicio); + break; + + + + case 'Idiomas R (UNAM)': + case 'Idiomas Sabatino': + const servActivosIdiomas = this.servActivosRepo.create({ + Red: true, + Correo: true, + usuario: user, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }); + await this.servActivosRepo.save(servActivosIdiomas); + + break; + + + + + case 'Reinscrito': + case 'Posgrado': + case 'Intercambio UNAM': + case 'Movilidad': + case 'Ampliación de Conocimiento': case 'Licenciatura': case 'Profesor': const servActivos = this.servActivosRepo.create({ @@ -324,7 +400,10 @@ export class ExcelService { } - return { inserted: rows.length, id_movimiento: movimiento.id_mov, errors: errs }; + + + + return { inserted: rows.length - contador, id_movimiento: movimiento.id_mov, errors: errs }; } @@ -558,7 +637,8 @@ export class ExcelService { }).join('\n'); const tablaCompleta = header + rows; - + console.log(tablaCompleta); + console.log('Usuarios encontrados:', usuarios); return tablaCompleta; } @@ -586,6 +666,7 @@ export class ExcelService { }, servActivo: { ATStatus: 'Inactivo', + AT: true }, }, relations: [ @@ -674,6 +755,50 @@ export class ExcelService { } + async enviarCargaMasiva(): Promise { + const usuarios_red = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'RED' } } }); + const usuarios_at = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'AT' } } }); + const usuarios_correo = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'CORREO' } } }); + const usuarios_solicita = await this.usuariosDelSistemaRepo.findOne({ where: { origen: { origen: 'SOLICITA' } } }); + if (!usuarios_red || !usuarios_at || !usuarios_correo || !usuarios_solicita) { + throw new BadRequestException('No se encontraron usuarios para enviar correo'); + } + + // Aquí puedes enviar el correo usando MailService + await this.mailService.sendMail({ + to: usuarios_red.correo ?? '', + subject: 'Carga de Usuarios', + text: 'Se ha realizado una carga de usuarios en el sistema.', + html: '', + }); + await this.mailService.sendMail({ + to: usuarios_at.correo ?? '', + subject: 'Carga de Usuarios', + text: 'Se ha realizado una carga de usuarios en el sistema.', + html: '', + }); + await this.mailService.sendMail({ + to: usuarios_correo.correo ?? '', + subject: 'Carga de Usuarios', + text: 'Se ha realizado una carga de usuarios en el sistema.', + html: '', + }); + await this.mailService.sendMail({ + to: usuarios_solicita.correo ?? '', + subject: 'Carga de Usuarios', + text: 'Se ha realizado una carga de usuarios en el sistema.', + html: '', + }); + + + + console.log(`Correo enviados`); + } + + + + + } diff --git a/src/mail/dto/send-email.dto.ts b/src/mail/dto/send-email.dto.ts new file mode 100644 index 0000000..1ecf6fe --- /dev/null +++ b/src/mail/dto/send-email.dto.ts @@ -0,0 +1,25 @@ +import { IsString, IsEmail, IsDate, IsOptional } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class SendCorreoDto { + @IsEmail() + to: string; + + @IsString() + @IsOptional() + subject: string; + + @IsString() + @IsOptional() + text: string; + + + + @IsString() + @IsOptional() + html: string; + + + + +} \ No newline at end of file diff --git a/src/mail/mail.controller.ts b/src/mail/mail.controller.ts new file mode 100644 index 0000000..bae8558 --- /dev/null +++ b/src/mail/mail.controller.ts @@ -0,0 +1,31 @@ +// src/excel/excel.controller.ts +import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus, Param, ParseIntPipe, Body } from '@nestjs/common'; +import { SendCorreoDto } from './dto/send-email.dto'; +import { MailService } from './mail.service'; + + +@Controller('mail') +export class MailController { + constructor( + private readonly mailService: MailService + ) { } + + @Post('send') + async sendMail(@Body() body: SendCorreoDto) { + const result = + await this.mailService.sendMail(body) + .then((value) => { + console.log("Se mandaron los correos", value); + + }) + .catch((err) => { + console.log(err) + }) + + + // sistem + return result; + + + } +} \ No newline at end of file diff --git a/src/mail/mail.module.ts b/src/mail/mail.module.ts new file mode 100644 index 0000000..2018716 --- /dev/null +++ b/src/mail/mail.module.ts @@ -0,0 +1,12 @@ +// src/movimiento/movimiento.module.ts +import { Module } from '@nestjs/common'; +import { MailService } from './mail.service'; +import { MailController } from './mail.controller'; + +@Module({ + imports: [], + controllers: [MailController], + providers: [MailService], + exports: [MailService], +}) +export class MailModule { } diff --git a/src/mail/mail.service.ts b/src/mail/mail.service.ts new file mode 100644 index 0000000..8b0a898 --- /dev/null +++ b/src/mail/mail.service.ts @@ -0,0 +1,76 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import * as nodemailer from 'nodemailer'; +import { SendCorreoDto } from 'src/mail/dto/send-email.dto'; + + + + +@Injectable() +export class MailService { + + async sendMail(sendEmail: SendCorreoDto) { + + + + + + + const transporter = nodemailer.createTransport({ + + + service: 'gmail', + auth: { + type: 'OAuth2', + user: process.env.EMAIL_USER, // tu correo de Gmail + clientId: process.env.CLIENT_ID, // tu Client ID de OAuth2 + clientSecret: process.env.CLIENT_SECRET, // tu Client Secret de OAuth2 + refreshToken: process.env.REFRESH_TOKEN, // tu Refresh Token de OAuth2 + //accessToken: sistem.accessToken, // opcional + }, + pool: true, + maxConnections: 1, + maxMessages: 100, + rateDelta: 2000, + rateLimit: 1 + + + }); + + + + + + + const mailOptions = { + from: process.env.EMAIL_USER, // tu correo de Gmail + to: sendEmail.to, + subject: sendEmail.subject, + text: sendEmail.text, + html: sendEmail.html, + }; + + + console.log(mailOptions); + + + + + let resMail = await transporter.sendMail(mailOptions); + if (!resMail) throw new Error("") + + + const statusTexto = resMail.accepted.length > 0 ? "Enviado" : "Fallido"; + + + + if (!resMail) { + throw new Error("fallo") + } + + + return (statusTexto); + } + + + +} diff --git a/src/movimiento/movimiento.module.ts b/src/movimiento/movimiento.module.ts index 7275540..c93d9ad 100644 --- a/src/movimiento/movimiento.module.ts +++ b/src/movimiento/movimiento.module.ts @@ -2,11 +2,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MovimientoService } from './movimiento.service'; -import { Movimiento, Origen } from '../entities/entities'; +import { Movimiento, Origen, Usuario } from '../entities/entities'; @Module({ - imports: [TypeOrmModule.forFeature([Movimiento, Origen])], + imports: [TypeOrmModule.forFeature([Movimiento, Origen, Usuario])], providers: [MovimientoService], exports: [MovimientoService], }) -export class MovimientoModule {} +export class MovimientoModule { } diff --git a/src/movimiento/movimiento.service.ts b/src/movimiento/movimiento.service.ts index f91e5fd..7bdacea 100644 --- a/src/movimiento/movimiento.service.ts +++ b/src/movimiento/movimiento.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Movimiento } from '../entities/entities'; +import { QueryRunner, Repository } from 'typeorm'; +import { Movimiento, Usuario } from '../entities/entities'; import { Origen } from '../entities/entities'; @Injectable() @@ -11,6 +11,8 @@ export class MovimientoService { private readonly movRepo: Repository, @InjectRepository(Origen) private readonly origenRepo: Repository, + @InjectRepository(Usuario) + private readonly usuarioRepo: Repository, ) { } /** Crea un registro de movimiento */ @@ -23,7 +25,14 @@ export class MovimientoService { ): Promise { // 1) Obtén el origen o falla si no existe const origin = await this.origenRepo.findOne({ where: { origen } }); - if (!origin) throw new Error(`Origen desconocido: ${origen}`); + let origenRepo = origin; + if (!origin) { + const newOrigin = this.origenRepo.create({ origen }); + origenRepo = await this.origenRepo.save(newOrigin); + } + if (!origenRepo) { + throw new Error(`No se pudo obtener o crear el origen: ${origen}`); + } // 2) Crea el objeto movimiento const movimiento = this.movRepo.create({ @@ -32,7 +41,7 @@ export class MovimientoService { observaciones, reporte, flag, - origen: { id_origen: origin.id_origen }, + origen: { id_origen: origenRepo.id_origen }, }); // 3) Guarda y devuelve el ID @@ -40,6 +49,35 @@ export class MovimientoService { return saved; } + async logger( + queryRunner: QueryRunner, + origenNombre: string, + status: string, + reporte: string | undefined, + observaciones: string + ): Promise { + const origenEntidad = await queryRunner.manager.findOne(Origen, { + where: { origen: origenNombre }, + }); + + if (!origenEntidad) { + throw new Error(`Origen no encontrado: ${origenNombre}`); + } + + const nuevoMov = queryRunner.manager.create(Movimiento, { + fecha_mov: new Date(), + status, + observaciones, + reporte, + origen: origenEntidad, + flag: false, + }); + + return await queryRunner.manager.save(nuevoMov); + } + + + /** Actualiza el status de un movimiento existente */ async updateStatus(id_movimiento: number, nuevoStatus: string, observaciones?: string): Promise { @@ -56,8 +94,149 @@ export class MovimientoService { await this.movRepo.save(movimiento); } - async findAll(): Promise { - return await this.movRepo.find({ where: { origen: { origen: 'LOAD' } } }); + async findAll(origen: string): Promise<{ move: any[]; button: boolean }> { + const statusFieldMap = { + AT: 'ATStatus', + RED: 'RedStatus', + CORREO: 'CorreoStatus', + SOLICITA: 'PrestamosStatus', + } as const; + + const FieldMap = { + AT: 'AT', + RED: 'Red', + CORREO: 'Correo', + SOLICITA: 'Prestamos', + } as const; + + + + const field = statusFieldMap[origen as keyof typeof statusFieldMap]; + + if (!field) { + const movimientos = await this.movRepo.find({ + where: { + origen: { origen: 'LOAD' }, + status: 'SUCCESS', + reporte: 'CARGA MASIVA DE USUARIOS', + + }, + }); + + const mov = await this.movRepo.find({ + where: { + origen: { origen: 'SISTEMA' }, + status: 'SUCCESS', + reporte: 'CARGA MASIVA WEB SERVICE', + + }, + }); + + const mov2 = await this.movRepo.find({ + where: { + + reporte: 'CARGA INDIVIDUAL DE USUARIOS', + status: 'SUCCESS', + + + }, + }); + console.log('movimientos:', mov2) + + + + + const todosLosMovimientos = [...movimientos, ...mov, ...mov2]; + console.log(todosLosMovimientos) + return { move: todosLosMovimientos, button: false }; + } + + const movimientos = await this.movRepo.find({ + where: { + origen: { origen: 'LOAD' }, + reporte: 'CARGA MASIVA DE USUARIOS', + status: 'SUCCESS', + usuario: { + servActivo: { + [field]: 'Inactivo' + }, + }, + }, + }); + const mov = await this.movRepo.find({ + where: { + origen: { origen: 'SISTEMA' }, + reporte: 'CARGA MASIVA WEB SERVICE', + status: 'SUCCESS', + usuario: { + servActivo: { + [field]: 'Inactivo' + }, + }, + }, + }); + const mov2 = await this.movRepo.find({ + where: { + + reporte: 'CARGA INDIVIDUAL DE USUARIOS', + status: 'SUCCESS', + usuario: { + servActivo: { + [field]: 'Inactivo' + }, + }, + }, + }); + const todosLosMovimientos = [...movimientos, ...mov, ...mov2]; + console.log(todosLosMovimientos, movimientos) + return { move: todosLosMovimientos, button: true }; + } + + async findAllCargaIndv(origen): Promise { + + const statusFieldMap = { + AT: 'ATStatus', + RED: 'RedStatus', + CORREO: 'CorreoStatus', + SOLICITA: 'PrestamosStatus', + } as const; + + const FieldMap = { + AT: 'AT', + RED: 'Red', + CORREO: 'Correo', + SOLICITA: 'Prestamos', + } as const; + + + + + + const field = statusFieldMap[origen as keyof typeof statusFieldMap]; + + const origenField = FieldMap[origen as keyof typeof FieldMap]; + + if (!field) { + throw new Error(`Origen desconocido: ${origen}`); + } + + const usuarios = await this.usuarioRepo.find({ + where: { + movimiento: { + + reporte: 'CARGA INDIVIDUAL DE USUARIOS', + }, + + servActivo: { + [field]: 'Inactivo', + [origenField]: true, + }, + + }, + }); + + + return usuarios; } diff --git a/src/usuarios/dto/create-usuario.dto.ts b/src/usuarios/dto/create-usuario.dto.ts index d7960fe..27578fa 100644 --- a/src/usuarios/dto/create-usuario.dto.ts +++ b/src/usuarios/dto/create-usuario.dto.ts @@ -1 +1,46 @@ -export class CreateUsuarioDto {} +import { IsNumber, IsOptional, IsString, Length } from "class-validator"; + +export class CreateUsuarioDto { + @IsOptional() + @IsString() + num_cuenta?: string; + + @IsOptional() + @IsString() + carrera?: string; + + + @IsString() + tipo_usuario: string; + + @IsOptional() + @IsString() + clave_carrera?: string; + + @IsString() + nombre: string; + + @IsString() + a_paterno: string; + + @IsString() + a_materno: string; + + @IsOptional() + @IsString() + @Length(10, 13) + rfc?: string; + + @IsOptional() + @IsString() + @Length(8, 8) + fecha_nacimiento?: string; + + @IsOptional() + @IsNumber() + generacion?: number; + + @IsOptional() + @IsString() + genero?: string; +} \ No newline at end of file diff --git a/src/usuarios/usuarios.controller.ts b/src/usuarios/usuarios.controller.ts index a0d1e65..21af1bf 100644 --- a/src/usuarios/usuarios.controller.ts +++ b/src/usuarios/usuarios.controller.ts @@ -11,11 +11,18 @@ export class UsuariosController { constructor(private readonly usuariosService: UsuariosService) { } + @Get('users') + finsdAll() { + return this.usuariosService.enviarSolicitud(); + } @Get(':noCuenta') findOne(@Param('noCuenta') noCuenta: string) { return this.usuariosService.findOneStatus(noCuenta); } + + + // @UseGuards(AuthGuard('jwt')) // @ApiBearerAuth('bearer') // @Post() diff --git a/src/usuarios/usuarios.module.ts b/src/usuarios/usuarios.module.ts index 1741048..ff4062c 100644 --- a/src/usuarios/usuarios.module.ts +++ b/src/usuarios/usuarios.module.ts @@ -2,18 +2,20 @@ import { Module } from '@nestjs/common'; import { UsuariosService } from './usuarios.service'; import { UsuariosController } from './usuarios.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Carrera, CarreraUsuario, Genero, Usuario } from 'src/entities/entities'; +import { Carrera, CarreraUsuario, Genero, TipoUsuario, Usuario, UsuariosDelSistema, UsuarioTipoUsuario } from 'src/entities/entities'; import { MovimientoModule } from 'src/movimiento/movimiento.module'; import { ServActivos } from './entities/servActivos.entitie'; +import { MailService } from 'src/mail/mail.service'; +import { ExcelService } from 'src/excel/excel.service'; @Module({ imports: [ - TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario, ServActivos]), + TypeOrmModule.forFeature([Usuario, Genero, Carrera, CarreraUsuario, ServActivos, TipoUsuario, UsuarioTipoUsuario, UsuariosDelSistema]), MovimientoModule ], controllers: [UsuariosController], - providers: [UsuariosService], + providers: [UsuariosService, MailService, ExcelService], exports: [TypeOrmModule.forFeature([ServActivos])], }) export class UsuariosModule { } diff --git a/src/usuarios/usuarios.service.ts b/src/usuarios/usuarios.service.ts index 251251c..65fe902 100644 --- a/src/usuarios/usuarios.service.ts +++ b/src/usuarios/usuarios.service.ts @@ -1,20 +1,42 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { CreateUsuarioDto } from './dto/create-usuario.dto'; import { UpdateUsuarioDto } from './dto/update-usuario.dto'; import { InjectRepository } from '@nestjs/typeorm'; -import { CarreraUsuario, Usuario } from 'src/entities/entities'; +import { Carrera, CarreraUsuario, Genero, TipoUsuario, Usuario, UsuarioTipoUsuario } from 'src/entities/entities'; import { Repository } from 'typeorm'; import { ServActivos } from './entities/servActivos.entitie'; +import { MovimientoService } from 'src/movimiento/movimiento.service'; +import { MailService } from 'src/mail/mail.service'; +import { error } from 'console'; +import { Cron } from '@nestjs/schedule'; +import { ExcelService } from 'src/excel/excel.service'; + + + @Injectable() export class UsuariosService { + private readonly logger = new Logger(UsuariosService.name); + constructor( @InjectRepository(Usuario) private readonly usuarioRepository: Repository, - @InjectRepository(CarreraUsuario) - private readonly carreraUsuarioRepository: Repository, + @InjectRepository(Carrera) + private readonly carreraRepository: Repository, @InjectRepository(ServActivos) private readonly servActivosRepository: Repository, + @InjectRepository(TipoUsuario) + private readonly tipoUsuarioRepository: Repository, + @InjectRepository(Genero) + private readonly generoRepository: Repository, + @InjectRepository(CarreraUsuario) + private readonly carreraUsuario: Repository, + @InjectRepository(UsuarioTipoUsuario) + private readonly usuarioTipoUsuario: Repository, + private readonly movimientoService: MovimientoService, + private readonly mailService: MailService, + private readonly excelService: ExcelService, // Asegúrate de importar el servicio de Excel + ) { } altaIndividual(createUsuarioDto: CreateUsuarioDto) { @@ -27,69 +49,461 @@ export class UsuariosService { select: ['id_usuario'], }); - - - - if (!usuario) { throw new NotFoundException('Usuario no encontrado'); } const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: usuario.id_usuario } }, - }); + if (!servActivo) { throw new NotFoundException('Servicios no encontrados para el usuario'); } + const resultado: { + servicio: string; + status: string; + fecha_activacion: string | null; + }[] = []; + const mapServicio = (nombre: string, estado: string | undefined) => { + if (!estado) return; - // Transformar a formato plano - let resultadoPlano = {}; - - if (servActivo.AT) { - resultadoPlano = { - AT: servActivo.ATStatus - } - } - if (servActivo.Red) { - resultadoPlano = { - ...resultadoPlano, - Red: servActivo.RedStatus - } - } - if (servActivo.Prestamos) { - resultadoPlano = { - ...resultadoPlano, - Prestamos: servActivo.PrestamosStatus - } - } - if (servActivo.Correo) { - resultadoPlano = { - ...resultadoPlano, - Correo: servActivo.CorreoStatus + if (estado.startsWith('Activo')) { + const fechaCompleta = estado.substring(7); // "2025-06-25T14:26:21.427Z" + const fechaSolo = fechaCompleta.substring(0, 10); // "2025-06-25" + resultado.push({ + servicio: nombre, + status: 'Activo', + fecha_activacion: fechaSolo, + }); + } else { + resultado.push({ + servicio: nombre, + status: 'Inactivo', + fecha_activacion: null, + }); } + }; + + mapServicio('Préstamo de equipo en CEDETEC', servActivo.ATStatus); + mapServicio('Red', servActivo.RedStatus); + mapServicio('Préstamo de equipo PC Puma', servActivo.PrestamosStatus); + mapServicio('Correo', servActivo.CorreoStatus); + + if (resultado.length === 0) { + return [{ + servicio: 'Ninguno', + status: 'No hay servicios activos para este usuario', + fecha_activacion: null, + }]; } - // Si no hay servicios activos, retornar un objeto vacío - if (Object.keys(resultadoPlano).length === 0) { - resultadoPlano = { - Servicios: 'No hay servicios activos para este usuario' - }; - } - console.log('Resultado plano:', resultadoPlano); - - return [resultadoPlano]; // importante: frontend espera array - - - - + return resultado; } + update(id: number, updateUsuarioDto: UpdateUsuarioDto) { return `This action updates a #${id} usuario`; } + + + + + + @Cron('0 6 27 * *') // minuto hora día mes díaDeSemana + async tareaMensual() { + this.logger.log('Ejecutando consulta mensual de profesores'); + await this.enviarSolicitud(); // tu función de importación + } + + async enviarSolicitud() { + const mov = await this.movimientoService.log( + 'SISTEMA', + 'LOADING', + 'Enviando solicitud de consulta masiva de profesores al WebService', + 'CARGA MASIVA WEB SERVICE' + ); + + interface DatosEntrada { + Nombre: string; + ApellidoPaterno: string; + ApellidoMaterno: string; + NumeroTrabajador: string; + RFC: string; + } + + interface DatosRespuesta { + numeroTrabajador: string; + nombre: string; + apellidoPaterno: string; + apellidoMaterno: string; + nombreCarrera: string; + fechaNacimiento: string; + rfc: string; + genero: string; + correoPersonal: string; + nombramiento: string; + unidadResponsable: string; + curp: string; + } + + const apiUrl = process.env.API_URL; + if (!apiUrl) { + console.error("API_URL no está definida en las variables de entorno"); + return; + } + + const entrada: DatosEntrada = { + Nombre: "", + ApellidoPaterno: "", + ApellidoMaterno: "", + NumeroTrabajador: "", + RFC: "" + }; + + try { + const response = await fetch(apiUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(entrada) + }); + + if (response.status === 400) { + const errorText = await response.text(); + console.error("Error 400:", errorText); + return; + } else if (response.status === 204) { + console.log("No se encontraron datos conforme a los criterios de búsqueda"); + return; + } + + if (!response.ok) throw new Error("Error en la petición: " + response.statusText); + + const json: DatosRespuesta[] = await response.json(); + + let nuevos = 0; + let actualizados = 0; + + let existe = await this.tipoUsuarioRepository.findOne({ where: { tipo_usuario: 'Profesor' } }); + if (!existe) { + const tipoUsuario = this.tipoUsuarioRepository.create({ tipo_usuario: 'Profesor' }); + existe = await this.tipoUsuarioRepository.save(tipoUsuario); + } + + for (const prof of json) { + try { + if (!prof.rfc || !prof.nombre) { + console.warn('Profesor con datos incompletos detectado:', prof); + continue; + } + + const existente = await this.usuarioRepository.findOne({ + where: { rfc: prof.rfc }, + relations: ['genero'] + }); + + if (!existente) { + let genero: Genero | null = null; + if (prof.genero === 'M' || prof.genero === 'F') { + const generoTexto = prof.genero === 'M' ? 'Masculino' : 'Femenino'; + genero = await this.generoRepository.findOne({ where: { genero: generoTexto } }); + if (!genero) { + genero = await this.generoRepository.create({ genero: generoTexto }); + await this.generoRepository.save(genero); + } + } + + let carrera = await this.carreraRepository.findOne({ where: { carrera: prof.nombreCarrera.trim() } }) + + if (!carrera) { + const nuevaCarrera = await this.carreraRepository.create({ carrera: prof.nombreCarrera.trim(), clave: '' }); + carrera = await this.carreraRepository.save(nuevaCarrera); + } + + + + const nuevo = await this.usuarioRepository.create({ + num_cuenta: prof.numeroTrabajador == '' ? undefined : prof.numeroTrabajador, + nombre: prof.nombre, + a_paterno: prof.apellidoPaterno, + a_materno: prof.apellidoMaterno, + rfc: prof.rfc, + fecha_nacimiento: prof.fechaNacimiento, + genero, + + movimiento: mov + }); + + const savedUser = await this.usuarioRepository.save(nuevo); + + const servActivo = await this.servActivosRepository.create({ + ATStatus: 'Inactivo', + RedStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + usuario: savedUser, + AT: true, + Red: prof.numeroTrabajador !== '', + Prestamos: true, + Correo: true + }); + + await this.servActivosRepository.save(servActivo); + + const userCarrera = await this.carreraUsuario.create({ carrera, usuario: savedUser }); + const usuarioTipo = await this.usuarioTipoUsuario.create({ tipoUsuario: existe, usuario: savedUser }); + + await this.carreraUsuario.save(userCarrera); + await this.usuarioTipoUsuario.save(usuarioTipo); + + nuevos++; + } else { + let cambios = false; + + if (prof.numeroTrabajador && existente.num_cuenta !== prof.numeroTrabajador) { + existente.num_cuenta = prof.numeroTrabajador; + cambios = true; + } + if (existente.nombre !== prof.nombre) { existente.nombre = prof.nombre; cambios = true; } + if (existente.a_paterno !== prof.apellidoPaterno) { existente.a_paterno = prof.apellidoPaterno; cambios = true; } + if (existente.a_materno !== prof.apellidoMaterno) { existente.a_materno = prof.apellidoMaterno; cambios = true; } + if (existente.rfc !== prof.rfc) { existente.rfc = prof.rfc; cambios = true; } + if (existente.fecha_nacimiento !== prof.fechaNacimiento) { existente.fecha_nacimiento = prof.fechaNacimiento; cambios = true; } + + if (cambios) { + await this.usuarioRepository.save(existente); + actualizados++; + } + } + + console.log(`Progreso -> Nuevos: ${nuevos}, Actualizados: ${actualizados}`); + } catch (innerError) { + console.error('Error procesando profesor:', prof, innerError); + } + } + this.movimientoService.updateStatus(mov.id_mov, 'SUCCESS', 'Carga masiva de web Service completa: ' + `Nuevos: ${nuevos}, Actualizados: ${actualizados}`) + if (!process.env.EMAIL_USER) { + throw new Error() + } + this.mailService.sendMail({ + to: process.env.EMAIL_USER, + subject: 'Carga de datos exitosa', + text: 'Carga masiva de web Service completa: ' + `Nuevos: ${nuevos}, Actualizados: ${actualizados}`, + html: '

' + }) + + this.excelService.enviarCargaMasiva(); + + return `Importación finalizada. Nuevos: ${nuevos}, Actualizados: ${actualizados}`; + + } catch (error) { + this.logger.error('Error al consultar profesores desde el WebService', error); + throw error; + } + } + + + async cargaIndividual(usuario: CreateUsuarioDto, origen: string) { + const queryRunner = this.usuarioRepository.manager.connection.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const existente = await queryRunner.manager.findOne(Usuario, { + where: { num_cuenta: usuario.num_cuenta }, + relations: ['genero'], + }); + + if (existente) { + throw new Error('Ya existe este usuario'); + } + + const mov = await this.movimientoService.logger(queryRunner, origen, 'LOADING', undefined, 'CARGA INDIVIDUAL DE USUARIOS'); + + let genero: Genero | null = null; + if (usuario.genero === 'M' || usuario.genero === 'F') { + const generoTexto = usuario.genero === 'M' ? 'Masculino' : 'Femenino'; + genero = await queryRunner.manager.findOne(Genero, { where: { genero: generoTexto } }); + if (!genero) { + genero = queryRunner.manager.create(Genero, { genero: generoTexto }); + await queryRunner.manager.save(genero); + } + } + + let carrera = await queryRunner.manager.findOne(Carrera, { where: { carrera: usuario.carrera } }); + if (!carrera) { + const nuevaCarrera = queryRunner.manager.create(Carrera, { carrera: usuario.carrera, clave: usuario.clave_carrera }); + carrera = await queryRunner.manager.save(nuevaCarrera); + } + + + const nuevo = queryRunner.manager.create(Usuario, { + num_cuenta: usuario.num_cuenta, + nombre: usuario.nombre, + a_paterno: usuario.a_paterno, + a_materno: usuario.a_materno, + rfc: usuario.rfc ?? null, + fecha_nacimiento: usuario.fecha_nacimiento, + generacion: usuario.generacion ?? null, + genero, + movimiento: mov, + }); + + const savedUser = await queryRunner.manager.save(nuevo); + + const userCarrera = queryRunner.manager.create(CarreraUsuario, { carrera, usuario: savedUser }); + + const FieldMap = [ + 'Diplomado', 'Extra Largo', 'Servicio Social', 'Idiomas R (UNAM)', 'Idiomas Sabatino', + 'Reinscrito', 'Posgrado', 'Intercambio UNAM', 'Movilidad', 'Ampliación de Conocimiento', + 'Licenciatura', 'Profesor', 'Trabajadores', + ]; + + if (!FieldMap.includes(usuario.tipo_usuario)) { + throw new Error(`Tipo de usuario no permitido: ${usuario.tipo_usuario}`); + } + + // Verifica si el tipo de usuario ya existe + let tipoUsuario = await queryRunner.manager.findOne(TipoUsuario, { + where: { tipo_usuario: usuario.tipo_usuario }, + }); + + // Si no existe, lo crea y guarda + if (!tipoUsuario) { + tipoUsuario = queryRunner.manager.create(TipoUsuario, { + tipo_usuario: usuario.tipo_usuario, + }); + tipoUsuario = await queryRunner.manager.save(tipoUsuario); + } + + // Luego crea la relación con el usuario ya guardado + const usuarioTipo = queryRunner.manager.create(UsuarioTipoUsuario, { + tipoUsuario, + usuario: savedUser, + }); + await queryRunner.manager.save(usuarioTipo); + + + + + + switch (usuario.tipo_usuario.trim()) { + + case 'Diplomado': + + const servActivosDiplomado = this.servActivosRepository.create({ + Correo: true, + usuario: savedUser, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }); + await queryRunner.manager.save(servActivosDiplomado); + + break; + + case 'Extra Largo': + const servActivosExtraLargo = this.servActivosRepository.create({ + Prestamos: true, + Correo: true, + usuario: savedUser, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + + }); + await queryRunner.manager.save(servActivosExtraLargo); + break; + + case 'Servicio Social': + const servActivosServicio = this.servActivosRepository.create({ + Red: true, + AT: true, + Correo: true, + usuario: savedUser, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }); + await queryRunner.manager.save(servActivosServicio); + break; + + + + case 'Idiomas R (UNAM)': + case 'Idiomas Sabatino': + const servActivosIdiomas = this.servActivosRepository.create({ + Red: true, + Correo: true, + usuario: savedUser, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }); + await queryRunner.manager.save(servActivosIdiomas); + + break; + + + + + case 'Reinscrito': + case 'Posgrado': + case 'Intercambio UNAM': + case 'Movilidad': + case 'Ampliación de Conocimiento': + case 'Licenciatura': + case 'Profesor': + const servActivos = this.servActivosRepository.create({ + + Red: true, + AT: true, + Correo: true, + Prestamos: true, + usuario: savedUser, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }); + await queryRunner.manager.save(servActivos); + + break; + + + + case 'Trabajadores': + const servActivosTrabajadores = this.servActivosRepository.create({ + Red: true, + usuario: savedUser, + RedStatus: 'Inactivo', + + }); + await queryRunner.manager.save(servActivosTrabajadores); + break; + } + + + console.log(savedUser) + await queryRunner.commitTransaction(); + + return { saved: savedUser, movId: mov.id_mov }; + + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } } + + From e72b71e4795fba8e5c957ae22448d558505e1ca0 Mon Sep 17 00:00:00 2001 From: evenegas Date: Mon, 30 Jun 2025 12:53:57 -0600 Subject: [PATCH 14/22] Se agregaron validaciones --- .env(Example) => .env(Example).txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .env(Example) => .env(Example).txt (100%) diff --git a/.env(Example) b/.env(Example).txt similarity index 100% rename from .env(Example) rename to .env(Example).txt From 1f76cb5c5b3e1752e3e75f5feecd91cbeb185dc0 Mon Sep 17 00:00:00 2001 From: evenegas Date: Mon, 30 Jun 2025 14:06:48 -0600 Subject: [PATCH 15/22] =?UTF-8?q?se=20cambio=20el=20m=C3=A9todo=20post?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/excel/excel.controller.ts | 2 ++ src/usuarios/usuarios.controller.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index d8bc7af..0268c2f 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -293,4 +293,6 @@ export class ExcelController { + + } \ No newline at end of file diff --git a/src/usuarios/usuarios.controller.ts b/src/usuarios/usuarios.controller.ts index 21af1bf..9625640 100644 --- a/src/usuarios/usuarios.controller.ts +++ b/src/usuarios/usuarios.controller.ts @@ -11,7 +11,8 @@ export class UsuariosController { constructor(private readonly usuariosService: UsuariosService) { } - @Get('users') + + @Post('users') finsdAll() { return this.usuariosService.enviarSolicitud(); } From 4510999453f835fa6ef6d408660c7ae7e779c634 Mon Sep 17 00:00:00 2001 From: evenegas Date: Tue, 1 Jul 2025 17:13:51 -0600 Subject: [PATCH 16/22] correcion en las funciones de web service, descarga y movimientos --- src/excel/excel.service.ts | 9 +- src/movimiento/movimiento.service.ts | 4 + src/usuarios/usuarios.service.ts | 151 ++++++++++++++++++++++++--- 3 files changed, 147 insertions(+), 17 deletions(-) diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index 1a36a1b..e536350 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -466,6 +466,7 @@ export class ExcelService { }, servActivo: { CorreoStatus: 'Inactivo', // solo los que no tienen servicio activo + Correo: true, // solo los que tienen el servicio de correo activo } }, relations: [ @@ -531,6 +532,7 @@ export class ExcelService { }, servActivo: { PrestamosStatus: 'Inactivo', // solo los que no tienen servicio activo + Prestamos: true, // solo los que tienen el servicio de préstamos activo } }, relations: [ @@ -612,6 +614,7 @@ export class ExcelService { }, servActivo: { RedStatus: 'Inactivo', + Red: true, // solo los que tienen el servicio de red activo } }, relations: [ @@ -682,7 +685,7 @@ export class ExcelService { const header = [ 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', - 'carreras', 'rfc', 'fecha_nacimiento', 'generacion' + 'carreras', 'fecha_nacimiento', 'generacion' ].join('\t') + '\n'; const rows = usuarios.map(u => { @@ -744,9 +747,7 @@ export class ExcelService { } else if (origen === 'SOLICITA' && servActivos.Prestamos) { servActivos.PrestamosStatus = timestamp; } else { - throw new BadRequestException( - `Origen ${origen} no válido o servicio no activo para el usuario con ID ${usuario.id_usuario}`, - ); + continue; } await this.servActivosRepo.save(servActivos); diff --git a/src/movimiento/movimiento.service.ts b/src/movimiento/movimiento.service.ts index 7bdacea..a55c978 100644 --- a/src/movimiento/movimiento.service.ts +++ b/src/movimiento/movimiento.service.ts @@ -112,6 +112,7 @@ export class MovimientoService { const field = statusFieldMap[origen as keyof typeof statusFieldMap]; + const origenField = FieldMap[origen as keyof typeof FieldMap]; if (!field) { const movimientos = await this.movRepo.find({ @@ -158,6 +159,7 @@ export class MovimientoService { status: 'SUCCESS', usuario: { servActivo: { + //[origenField]:true, [field]: 'Inactivo' }, }, @@ -170,6 +172,7 @@ export class MovimientoService { status: 'SUCCESS', usuario: { servActivo: { + //[origenField]:true, [field]: 'Inactivo' }, }, @@ -182,6 +185,7 @@ export class MovimientoService { status: 'SUCCESS', usuario: { servActivo: { + //[origenField]:true, [field]: 'Inactivo' }, }, diff --git a/src/usuarios/usuarios.service.ts b/src/usuarios/usuarios.service.ts index 65fe902..aa5e342 100644 --- a/src/usuarios/usuarios.service.ts +++ b/src/usuarios/usuarios.service.ts @@ -87,10 +87,13 @@ export class UsuariosService { } }; - mapServicio('Préstamo de equipo en CEDETEC', servActivo.ATStatus); - mapServicio('Red', servActivo.RedStatus); - mapServicio('Préstamo de equipo PC Puma', servActivo.PrestamosStatus); - mapServicio('Correo', servActivo.CorreoStatus); + + await mapServicio('Red', servActivo.RedStatus); + await mapServicio('Correo', servActivo.CorreoStatus); + await mapServicio('Préstamo de equipo PC Puma', servActivo.PrestamosStatus); + await mapServicio('Préstamo de equipo en CEDETEC', servActivo.ATStatus); + + if (resultado.length === 0) { return [{ @@ -100,6 +103,8 @@ export class UsuariosService { }]; } + console.log('Servicios encontrados:', resultado); + return resultado; } @@ -128,6 +133,7 @@ export class UsuariosService { 'CARGA MASIVA WEB SERVICE' ); + interface DatosEntrada { Nombre: string; ApellidoPaterno: string; @@ -246,9 +252,9 @@ export class UsuariosService { PrestamosStatus: 'Inactivo', CorreoStatus: 'Inactivo', usuario: savedUser, - AT: true, + AT: prof.numeroTrabajador !== '', Red: prof.numeroTrabajador !== '', - Prestamos: true, + Prestamos: prof.numeroTrabajador !== '', Correo: true }); @@ -264,18 +270,137 @@ export class UsuariosService { } else { let cambios = false; - if (prof.numeroTrabajador && existente.num_cuenta !== prof.numeroTrabajador) { - existente.num_cuenta = prof.numeroTrabajador; + + if (existente.nombre !== prof.nombre) { + existente.nombre = prof.nombre; + const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } }); + + + if (servActivo) { + servActivo.Red = true; + servActivo.AT = true; + servActivo.Prestamos = true; + + + servActivo.ATStatus = 'Inactivo'; + servActivo.PrestamosStatus = 'Inactivo'; + servActivo.RedStatus = 'Inactivo'; + await this.servActivosRepository.save(servActivo); + } else { + console.warn('No se encontró el servicio activo para el usuario:', existente); + } + cambios = true; + } + + if (existente.a_paterno !== prof.apellidoPaterno) { + existente.a_paterno = prof.apellidoPaterno; + const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } }); + + if (servActivo) { + servActivo.Red = true; + servActivo.AT = true; + servActivo.Prestamos = true; + + + servActivo.ATStatus = 'Inactivo'; + servActivo.PrestamosStatus = 'Inactivo'; + servActivo.RedStatus = 'Inactivo'; + await this.servActivosRepository.save(servActivo); + } else { + console.warn('No se encontró el servicio activo para el usuario:', existente); + } + + cambios = true; + } + + + if (existente.a_materno !== prof.apellidoMaterno) { + + existente.a_materno = prof.apellidoMaterno; + const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } }); + + if (servActivo) { + servActivo.Red = true; + servActivo.AT = true; + servActivo.Prestamos = true; + + + servActivo.ATStatus = 'Inactivo'; + servActivo.PrestamosStatus = 'Inactivo'; + servActivo.RedStatus = 'Inactivo'; + await this.servActivosRepository.save(servActivo); + } else { + console.warn('No se encontró el servicio activo para el usuario:', existente); + } + + cambios = true; + } + + if (existente.rfc !== prof.rfc) { + existente.rfc = prof.rfc; + const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } }); + + if (servActivo) { + servActivo.Red = true; + + servActivo.Prestamos = true; + + + servActivo.PrestamosStatus = 'Inactivo'; + servActivo.RedStatus = 'Inactivo'; + + await this.servActivosRepository.save(servActivo); + } else { + console.warn('No se encontró el servicio activo para el usuario:', existente); + } + cambios = true; + } + + if (existente.fecha_nacimiento !== prof.fechaNacimiento) { + existente.fecha_nacimiento = prof.fechaNacimiento; + const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } }); + + if (servActivo) { + + + servActivo.Prestamos = true; + servActivo.Correo = true; + servActivo.PrestamosStatus = 'Inactivo'; + servActivo.CorreoStatus = 'Inactivo'; + + await this.servActivosRepository.save(servActivo); + } else { + console.warn('No se encontró el servicio activo para el usuario:', existente); + } + cambios = true; + } + + if (prof.numeroTrabajador && existente.num_cuenta !== prof.numeroTrabajador) { + existente.num_cuenta = prof.numeroTrabajador; + + + const servActivo = await this.servActivosRepository.findOne({ where: { usuario: { id_usuario: existente.id_usuario } } }); + if (servActivo) { + servActivo.Red = true; + servActivo.AT = true; + servActivo.Correo = true; + servActivo.Prestamos = true; + + servActivo.CorreoStatus = 'Inactivo'; + servActivo.ATStatus = 'Inactivo'; + servActivo.PrestamosStatus = 'Inactivo'; + servActivo.RedStatus = 'Inactivo'; + await this.servActivosRepository.save(servActivo); + } else { + console.warn('No se encontró el servicio activo para el usuario:', existente); + } cambios = true; } - if (existente.nombre !== prof.nombre) { existente.nombre = prof.nombre; cambios = true; } - if (existente.a_paterno !== prof.apellidoPaterno) { existente.a_paterno = prof.apellidoPaterno; cambios = true; } - if (existente.a_materno !== prof.apellidoMaterno) { existente.a_materno = prof.apellidoMaterno; cambios = true; } - if (existente.rfc !== prof.rfc) { existente.rfc = prof.rfc; cambios = true; } - if (existente.fecha_nacimiento !== prof.fechaNacimiento) { existente.fecha_nacimiento = prof.fechaNacimiento; cambios = true; } if (cambios) { + existente.movimiento = mov; await this.usuarioRepository.save(existente); + actualizados++; } } From 95d4fa506cf5fcaac1cbaed413bfd1791ddcda2d Mon Sep 17 00:00:00 2001 From: evenegas Date: Wed, 2 Jul 2025 10:19:00 -0600 Subject: [PATCH 17/22] se cambio la descarga de a csv y subida de archivos --- src/excel/excel.controller.ts | 26 +-- src/excel/excel.service.ts | 390 +++++++++++++++------------------- 2 files changed, 184 insertions(+), 232 deletions(-) diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index 0268c2f..ca9bcf3 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -148,13 +148,13 @@ export class ExcelController { origen, 'SUCCESS', - undefined, + 'descarga de csv', `size=${csv.length}` ); return res .status(HttpStatus.OK) - .header('Content-Type', 'text/tab-separated-values') - .header('Content-Disposition', 'attachment; filename="usuarios_solicita.tsv"') + .header('Content-Type', 'text/csv') + .header('Content-Disposition', 'attachment; filename="usuarios_solicita.csv"') .send(csv); } catch (err) { await this.movimientoService.log( @@ -166,20 +166,20 @@ export class ExcelController { .status(HttpStatus.FORBIDDEN) .json({ statusCode: 403, message: 'Origen no permitido para descarga.' }); } - } else if (origen == 'CORREO') { + } else if (origen == 'CORREO' || origen == 'LOAD') { try { const csv = await this.excelService.generateCsvCorreo(); await this.movimientoService.log( origen, 'SUCCESS', - undefined, + 'descarga de csv', `size=${csv.length}` ); return res .status(HttpStatus.OK) - .header('Content-Type', 'text/tab-separated-values') - .header('Content-Disposition', 'attachment; filename="usuarios.tsv"') + .header('Content-Type', 'text/csv') + .header('Content-Disposition', 'attachment; filename="usuarios_correo.csv"') .send(csv); } catch (err) { await this.movimientoService.log( @@ -199,13 +199,13 @@ export class ExcelController { origen, 'SUCCESS', - undefined, + 'descarga de csv', `size=${csv.length}` ); return res .status(HttpStatus.OK) - .header('Content-Type', 'text/tab-separated-values') - .header('Content-Disposition', 'attachment; filename="usuarios_at.tsv"') + .header('Content-Type', 'text/csv') + .header('Content-Disposition', 'attachment; filename="usuarios_AT.csv"') .send(csv); } catch (err) { await this.movimientoService.log( @@ -225,13 +225,13 @@ export class ExcelController { origen, 'SUCCESS', - undefined, + 'descarga de csv', `size=${csv.length}` ); return res .status(HttpStatus.OK) - .header('Content-Type', 'text/tab-separated-values') - .header('Content-Disposition', 'attachment; filename="usuarios_red.tsv"') + .header('Content-Type', 'text/csv') + .header('Content-Disposition', 'attachment; filename="usuarios_red.csv"') .send(csv); } catch (err) { await this.movimientoService.log( diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index e536350..db8ab59 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -1,7 +1,7 @@ // src/excel/excel.service.ts import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { Workbook } from 'exceljs'; -import { In, Repository } from 'typeorm'; +import { DataSource, In, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { CarreraUsuario, TipoUsuario, Usuario, UsuariosDelSistema, UsuarioTipoUsuario } from '../entities/entities'; import { Genero } from '../entities/entities'; @@ -48,7 +48,8 @@ export class ExcelService { @InjectRepository(UsuariosDelSistema) private readonly usuariosDelSistemaRepo: Repository, private readonly movimientoService: MovimientoService, - private readonly mailService: MailService + private readonly mailService: MailService, + private readonly dataSource: DataSource // … inyecta otros repositorios si los usarás ) { } @@ -118,7 +119,7 @@ export class ExcelService { const rowNum = i + 2; // por el encabezado // Campos que no pueden quedar vacíos - ['cuenta', 'nombreCompleto', 'tipo'].forEach(field => { + ['cuenta', 'tipo'].forEach(field => { const raw = (r as any)[field]; const str = raw != null ? raw.toString().trim() : ''; if (!str) { @@ -144,7 +145,7 @@ export class ExcelService { seen.add(cuentaStr); } - // Numérico en cuenta y generación + const genStr = r.gen != null ? r.gen.toString().trim() : ''; @@ -201,215 +202,166 @@ export class ExcelService { const status = this.validateRows(file); const errs = status.errors; const rows = status.rowsGood; - let carga - if (rows.length == 0) { + + if (rows.length === 0) { throw new BadRequestException('No se encontraron filas válidas para cargar. ' + errs); } + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); - const movimiento = await this.movimientoService.log( + try { + const movimiento = await this.movimientoService.logger( + queryRunner, + 'LOAD', + 'LOADING', + undefined, + 'CARGA MASIVA DE USUARIOS' + ); - 'LOAD', - 'LOADING', - undefined, - `CARGA MASIVA DE USUARIOS`, - true, // flag para indicar que es una carga masiva - ); + let contador = 0; + for (const r of rows) { + const whereCond: Array<{ num_cuenta?: string; rfc?: string }> = [{ num_cuenta: r.cuenta }]; + if (r.rfc) { + whereCond.push({ rfc: r.rfc }); + } - let contador: number = 0; - - for (const r of rows) { - //0.1 - const userExists = await this.usuarioRepo.findOne({ - where: { num_cuenta: r.cuenta }, - }); - - if (userExists) { - errs.push(`Cuenta ${r.cuenta} ya existe, se omite.`); - contador++; - continue; // si ya existe, no lo insertamos - } - - - // 1) Género (crea o reutiliza) - let genero = await this.generoRepo.findOne({ where: { genero: r.sexo } }); - if (!genero) { - genero = this.generoRepo.create({ genero: r.sexo }); - await this.generoRepo.save(genero); - } - - // 2) Carrera (por clave) - let carrera = await this.carreraRepo.findOne({ - where: { clave: r.clave }, - }); - if (!carrera) { - carrera = this.carreraRepo.create({ - clave: r.clave, - carrera: r.nomCarr.slice(0, 100), + const userExists = await queryRunner.manager.findOne(Usuario, { + where: whereCond, }); - await this.carreraRepo.save(carrera); - } - let tipo = await this.tipoUsuarioRepo.findOne({ - where: { tipo_usuario: r.tipo.trim() }, - }); - if (!tipo) { - tipo = this.tipoUsuarioRepo.create({ - tipo_usuario: r.tipo.trim(), + + + if (userExists) { + errs.push(`Cuenta ${r.cuenta} ya existe, se omite.`); + contador++; + continue; + } + + if (!r.tipo || !r.nombres || !r.apellidopa || !r.apellidoma) { + errs.push(`Fila con cuenta ${r.cuenta} tiene campos faltantes.`); + contador++; + continue; + } + + let genero = await queryRunner.manager.findOne(Genero, { where: { genero: r.sexo } }); + if (!genero) { + genero = queryRunner.manager.create(Genero, { genero: r.sexo }); + await queryRunner.manager.save(genero); + } + + let carrera + if (r.clave || r.nomCarr) { + carrera = await queryRunner.manager.findOne(Carrera, { where: { clave: r.clave } }); + if (!carrera) { + carrera = queryRunner.manager.create(Carrera, { + clave: r.clave, + carrera: r.nomCarr.slice(0, 100), + }); + await queryRunner.manager.save(carrera); + } + + } else { + carrera = null; + } + + let tipo = await queryRunner.manager.findOne(TipoUsuario, { + where: { tipo_usuario: r.tipo.trim() }, }); - await this.tipoUsuarioRepo.save(tipo); + if (!tipo) { + tipo = queryRunner.manager.create(TipoUsuario, { tipo_usuario: r.tipo.trim() }); + await queryRunner.manager.save(tipo); + } + + const generacion = /^[0-9]{4}$/.test(r.gen?.toString().trim()) + ? parseInt(r.gen.toString().trim(), 10) + : null; + + const usuario = queryRunner.manager.create(Usuario, { + num_cuenta: r.cuenta, + nombre: r.nombres.trim().split(' ')[0], + a_paterno: r.apellidopa, + a_materno: r.apellidoma, + rfc: r.rfc, + fecha_nacimiento: r.fechnac, + generacion, + genero, + movimiento, + }); + const user = await queryRunner.manager.save(usuario); + + if (carrera) { + const carreraUsuario = queryRunner.manager.create(CarreraUsuario, { + usuario: user, + carrera, + }); + await queryRunner.manager.save(carreraUsuario); + } + + const usuarioTipo = queryRunner.manager.create(UsuarioTipoUsuario, { + usuario: user, + tipoUsuario: tipo, + }); + await queryRunner.manager.save(usuarioTipo); + + const servData = { + usuario: user, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }; + + switch (r.tipo.trim()) { + case 'Diplomado': + Object.assign(servData, { Correo: true }); + break; + case 'Extra Largo': + Object.assign(servData, { Correo: true, Prestamos: true }); + break; + case 'Servicio Social': + Object.assign(servData, { Correo: true, AT: true, Red: true }); + break; + case 'Idiomas R (UNAM)': + case 'Idiomas Sabatino': + Object.assign(servData, { Correo: true, Red: true }); + break; + case 'Reinscrito': + case 'Posgrado': + case 'Intercambio UNAM': + case 'Movilidad': + case 'Ampliación de Conocimiento': + case 'Licenciatura': + case 'Profesor': + Object.assign(servData, { Correo: true, AT: true, Red: true, Prestamos: true }); + break; + case 'Trabajadores': + Object.assign(servData, { Red: true }); + break; + } + + const serv = queryRunner.manager.create(ServActivos, servData); + await queryRunner.manager.save(serv); } - 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, - nombre: r.nombres.trim().split(' ')[0], // ajusta si quieres - a_paterno: r.apellidopa, - a_materno: r.apellidoma, - rfc: r.rfc, - fecha_nacimiento: r.fechnac, - generacion: generacion, - genero, - movimiento: { id_mov: movimiento.id_mov }, // Relación con movimiento - - }); - const user = await this.usuarioRepo.save(usuario); - - - - // 4) Relacion usuario–carrxera (tabla intermedia) - const carreraUsuario = this.carreraUsuarioRepo.create({ - usuario, - carrera, - }); - await this.carreraUsuarioRepo.save(carreraUsuario); - - // 5) Tipo de usuario - - const usuarioTipo = this.usuarioTipoUsuarioRepo.create({ - usuario, - tipoUsuario: tipo, - }); - await this.usuarioTipoUsuarioRepo.save(usuarioTipo); - - // 6) ServActivos (si es necesario) - switch (r.tipo.trim()) { - - case 'Diplomado': - const servActivosDiplomado = this.servActivosRepo.create({ - Correo: true, - usuario: user, - RedStatus: 'Inactivo', - ATStatus: 'Inactivo', - CorreoStatus: 'Inactivo', - PrestamosStatus: 'Inactivo', - }); - await this.servActivosRepo.save(servActivosDiplomado); - - break; - - case 'Extra Largo': - const servActivosExtraLargo = this.servActivosRepo.create({ - Prestamos: true, - Correo: true, - usuario: user, - RedStatus: 'Inactivo', - ATStatus: 'Inactivo', - CorreoStatus: 'Inactivo', - PrestamosStatus: 'Inactivo', - }); - await this.servActivosRepo.save(servActivosExtraLargo); - break; - - case 'Servicio Social': - const servActivosServicio = this.servActivosRepo.create({ - Red: true, - AT: true, - Correo: true, - usuario: user, - RedStatus: 'Inactivo', - ATStatus: 'Inactivo', - CorreoStatus: 'Inactivo', - PrestamosStatus: 'Inactivo', - }); - await this.servActivosRepo.save(servActivosServicio); - break; - - - - case 'Idiomas R (UNAM)': - case 'Idiomas Sabatino': - const servActivosIdiomas = this.servActivosRepo.create({ - Red: true, - Correo: true, - usuario: user, - RedStatus: 'Inactivo', - ATStatus: 'Inactivo', - CorreoStatus: 'Inactivo', - PrestamosStatus: 'Inactivo', - }); - await this.servActivosRepo.save(servActivosIdiomas); - - break; - - - - - case 'Reinscrito': - case 'Posgrado': - case 'Intercambio UNAM': - case 'Movilidad': - case 'Ampliación de Conocimiento': - case 'Licenciatura': - case 'Profesor': - const servActivos = this.servActivosRepo.create({ - - Red: true, - AT: true, - Correo: true, - Prestamos: true, - usuario: user, - RedStatus: 'Inactivo', - ATStatus: 'Inactivo', - CorreoStatus: 'Inactivo', - PrestamosStatus: 'Inactivo', - }); - await this.servActivosRepo.save(servActivos); - - break; - - - - case 'Trabajadores': - const servActivosTrabajadores = this.servActivosRepo.create({ - Red: true, - usuario: user, - RedStatus: 'Inactivo', - - }); - await this.servActivosRepo.save(servActivosTrabajadores); - break; - } - - + await queryRunner.commitTransaction(); + return { inserted: rows.length - contador, id_movimiento: movimiento.id_mov, errors: errs }; + } catch (err) { + await queryRunner.rollbackTransaction(); + throw err; + } finally { + await queryRunner.release(); } - - - - return { inserted: rows.length - contador, id_movimiento: movimiento.id_mov, errors: errs }; } + // async generateCsv(): Promise { // const users = await this.usuarioRepo.find({ // relations: ['genero', 'carreraUsuarios'], // si quieres más info @@ -483,7 +435,7 @@ export class ExcelService { const header = [ 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', 'carreras', 'rfc', 'fecha_nacimiento', 'generacion' - ].join('\t') + '\n'; + ].join(',') + '\n'; const rows = usuarios.map(u => { const carreras = (u.carreraUsuarios ?? []) @@ -491,15 +443,15 @@ export class ExcelService { .join(', '); // une todas las carreras en un solo string return [ - u.num_cuenta, - u.nombre, - u.a_paterno, - u.a_materno, - carreras, + u.num_cuenta ?? '', + u.nombre ?? '', + u.a_paterno ?? '', + u.a_materno ?? '', + carreras ?? '', u.rfc ?? '', - u.fecha_nacimiento, + u.fecha_nacimiento ?? '', u.generacion?.toString() ?? '' - ].join('\t'); + ].join(','); }).join('\n'); const tablaCompleta = header + rows; @@ -565,7 +517,7 @@ export class ExcelService { const header = [ 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', 'carreras', 'rfc' - ].join('\t') + '\n'; + ].join(',') + '\n'; const rows = usuarios.map(u => { const carreras = (u.carreraUsuarios ?? []) @@ -573,13 +525,13 @@ export class ExcelService { .join(', '); // une todas las carreras en un solo string return [ - u.num_cuenta, - u.nombre, - u.a_paterno, - u.a_materno, - carreras, + u.num_cuenta ?? '', + u.nombre ?? '', + u.a_paterno ?? '', + u.a_materno ?? '', + carreras ?? '', u.rfc ?? '', - ].join('\t'); + ].join(','); }).join('\n'); const tablaCompleta = header + rows; @@ -630,13 +582,13 @@ export class ExcelService { const header = [ 'num_cuenta', 'fecha_nacimiento' - ].join('\t') + '\n'; + ].join(',') + '\n'; const rows = usuarios.map(u => { return [ u.num_cuenta, u.fecha_nacimiento, - ].join('\t'); + ].join(','); }).join('\n'); const tablaCompleta = header + rows; @@ -686,22 +638,22 @@ export class ExcelService { const header = [ 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', 'carreras', 'fecha_nacimiento', 'generacion' - ].join('\t') + '\n'; + ].join(',') + '\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 + .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.num_cuenta ?? '', + u.nombre ?? '', + u.a_paterno ?? '', + u.a_materno ?? '', + carreras ?? '', + u.fecha_nacimiento ?? '', u.generacion?.toString() ?? '' - ].join('\t'); + ].join(','); }).join('\n'); const tablaCompleta = header + rows; From 780bde0b61d9cdf7f5cdeb0241c75f6e9362993c Mon Sep 17 00:00:00 2001 From: evenegas Date: Wed, 2 Jul 2025 15:48:36 -0600 Subject: [PATCH 18/22] se hicieron cambios --- src/movimiento/movimiento.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/movimiento/movimiento.service.ts b/src/movimiento/movimiento.service.ts index a55c978..12e2080 100644 --- a/src/movimiento/movimiento.service.ts +++ b/src/movimiento/movimiento.service.ts @@ -159,7 +159,8 @@ export class MovimientoService { status: 'SUCCESS', usuario: { servActivo: { - //[origenField]:true, + [origenField]: true, + [field]: 'Inactivo' }, }, @@ -172,7 +173,7 @@ export class MovimientoService { status: 'SUCCESS', usuario: { servActivo: { - //[origenField]:true, + [origenField]: true, [field]: 'Inactivo' }, }, @@ -185,7 +186,7 @@ export class MovimientoService { status: 'SUCCESS', usuario: { servActivo: { - //[origenField]:true, + [origenField]: true, [field]: 'Inactivo' }, }, From 100695148ad4ea606b3cd35b7a26571c053bf48a Mon Sep 17 00:00:00 2001 From: evenegas Date: Thu, 3 Jul 2025 11:14:35 -0600 Subject: [PATCH 19/22] se hicieron cambios --- src/app.module.ts | 2 +- src/excel/excel.controller.ts | 11 ++++++++++- src/main.ts | 8 ++++---- src/movimiento/movimiento.service.ts | 7 ++++--- src/usuarios/usuarios.service.ts | 9 +++++---- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index a98f4f9..ddda6df 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -31,7 +31,7 @@ import { ScheduleModule } from '@nestjs/schedule'; // no prints en claro en prod: password: config.get('DB_PASS', ''), database: config.get('DB_NAME', 'test'), - dropSchema: false, // solo para desarrollo + dropSchema: true, // solo para desarrollo }; Logger.log('[DB CONFIG] ' + JSON.stringify({ ...dbConfig, diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index ca9bcf3..1554c5e 100644 --- a/src/excel/excel.controller.ts +++ b/src/excel/excel.controller.ts @@ -103,6 +103,7 @@ export class ExcelController { return result } + console.log(result.id_movimiento) await this.movimientoService.updateStatus( result.id_movimiento, 'SUCCESS', @@ -176,10 +177,18 @@ export class ExcelController { 'descarga de csv', `size=${csv.length}` ); + let filename + if (origen == 'CORREO') { + filename = 'usuarios_correo.csv'; + } + else { + filename = 'usuarios.csv'; + } + return res .status(HttpStatus.OK) .header('Content-Type', 'text/csv') - .header('Content-Disposition', 'attachment; filename="usuarios_correo.csv"') + .header('Content-Disposition', `attachment; filename="${filename}"`) .send(csv); } catch (err) { await this.movimientoService.log( diff --git a/src/main.ts b/src/main.ts index 96bd98b..5eeb055 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,7 @@ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); - app.enableCors(); + app.enableCors({ exposedHeaders: ['Content-Disposition'] }); app.useGlobalPipes( new ValidationPipe({ @@ -23,9 +23,9 @@ async function bootstrap() { ) .setVersion('1.0') .addBearerAuth( - { - type: 'http', - scheme: 'bearer', + { + type: 'http', + scheme: 'bearer', bearerFormat: 'JWT', name: 'Authorization', in: 'header', diff --git a/src/movimiento/movimiento.service.ts b/src/movimiento/movimiento.service.ts index 12e2080..337632c 100644 --- a/src/movimiento/movimiento.service.ts +++ b/src/movimiento/movimiento.service.ts @@ -53,8 +53,8 @@ export class MovimientoService { queryRunner: QueryRunner, origenNombre: string, status: string, - reporte: string | undefined, - observaciones: string + observaciones?: string, + reporte?: string ): Promise { const origenEntidad = await queryRunner.manager.findOne(Origen, { where: { origen: origenNombre }, @@ -191,9 +191,10 @@ export class MovimientoService { }, }, }, + relations: ['usuario'], }); const todosLosMovimientos = [...movimientos, ...mov, ...mov2]; - console.log(todosLosMovimientos, movimientos) + return { move: todosLosMovimientos, button: true }; } diff --git a/src/usuarios/usuarios.service.ts b/src/usuarios/usuarios.service.ts index aa5e342..9ecb9d6 100644 --- a/src/usuarios/usuarios.service.ts +++ b/src/usuarios/usuarios.service.ts @@ -46,7 +46,7 @@ export class UsuariosService { async findOneStatus(noCuenta: string) { const usuario = await this.usuarioRepository.findOne({ where: { num_cuenta: noCuenta }, - select: ['id_usuario'], + }); if (!usuario) { @@ -103,9 +103,10 @@ export class UsuariosService { }]; } - console.log('Servicios encontrados:', resultado); - return resultado; + console.log({ resultado, usuario: usuario.num_cuenta }) + + return { resultado, usuario: usuario.num_cuenta }; } @@ -119,7 +120,7 @@ export class UsuariosService { - @Cron('0 6 27 * *') // minuto hora día mes díaDeSemana + @Cron('0 0 0 10,25 * *') async tareaMensual() { this.logger.log('Ejecutando consulta mensual de profesores'); await this.enviarSolicitud(); // tu función de importación From 772a4e23444df233a9620fe328b77f7f2a31a5f0 Mon Sep 17 00:00:00 2001 From: evenegas Date: Thu, 3 Jul 2025 11:14:39 -0600 Subject: [PATCH 20/22] se hicieron cambios --- src/usuarios/usuarios.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usuarios/usuarios.service.ts b/src/usuarios/usuarios.service.ts index 9ecb9d6..996e270 100644 --- a/src/usuarios/usuarios.service.ts +++ b/src/usuarios/usuarios.service.ts @@ -104,7 +104,7 @@ export class UsuariosService { } - console.log({ resultado, usuario: usuario.num_cuenta }) + return { resultado, usuario: usuario.num_cuenta }; } From c08dc2e7e3717bd2ccbc25f60995f65042a15b4f Mon Sep 17 00:00:00 2001 From: evenegas Date: Thu, 3 Jul 2025 12:26:02 -0600 Subject: [PATCH 21/22] se hicieron cambios --- src/app.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.module.ts b/src/app.module.ts index ddda6df..a98f4f9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -31,7 +31,7 @@ import { ScheduleModule } from '@nestjs/schedule'; // no prints en claro en prod: password: config.get('DB_PASS', ''), database: config.get('DB_NAME', 'test'), - dropSchema: true, // solo para desarrollo + dropSchema: false, // solo para desarrollo }; Logger.log('[DB CONFIG] ' + JSON.stringify({ ...dbConfig, From 4be22ac05475c36d019d0f99dd3cf7d5cd76afed Mon Sep 17 00:00:00 2001 From: evenegas Date: Fri, 4 Jul 2025 10:18:35 -0600 Subject: [PATCH 22/22] se hicieron cambios --- src/excel/excel.service.ts | 78 ++++++++++++++++---------------- src/usuarios/usuarios.service.ts | 2 +- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index db8ab59..05e156c 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -417,46 +417,49 @@ export class ExcelService { }, }, servActivo: { - CorreoStatus: 'Inactivo', // solo los que no tienen servicio activo - Correo: true, // solo los que tienen el servicio de correo activo - } + CorreoStatus: 'Inactivo', + Correo: true, + }, }, relations: [ 'genero', - 'carreraUsuarios', // relación intermedia - 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia + 'carreraUsuarios', + 'carreraUsuarios.carrera', + 'usuarioTipos', + 'usuarioTipos.tipoUsuario', ], - }); - - - const header = [ 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', - 'carreras', 'rfc', 'fecha_nacimiento', 'generacion' + 'carreras', 'rfc', 'fecha_nacimiento', 'generacion', 'tipos_usuario' ].join(',') + '\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 + .map(cu => cu.carrera?.carrera ?? '') + .join(' | '); + + const tiposUsuario = (u.usuarioTipos ?? []) + .map(utu => utu.tipoUsuario?.tipo_usuario ?? '') + .join(' | '); return [ u.num_cuenta ?? '', u.nombre ?? '', u.a_paterno ?? '', u.a_materno ?? '', - carreras ?? '', + carreras, u.rfc ?? '', u.fecha_nacimiento ?? '', - u.generacion?.toString() ?? '' + u.generacion?.toString() ?? '', + tiposUsuario ].join(','); }).join('\n'); const tablaCompleta = header + rows; - return tablaCompleta; + } async generateCsvSolicita(): Promise { @@ -491,38 +494,24 @@ export class ExcelService { 'genero', 'carreraUsuarios', // relación intermedia 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia + 'usuarioTipos', + 'usuarioTipos.tipoUsuario', ], }); - - const usuariosTipo = await this.usuarioRepo.find({ - - relations: [ - 'genero', - 'carreraUsuarios', // relación intermedia - 'carreraUsuarios.carrera', - 'usuarioTipos', // relación con tipo de usuario - 'usuarioTipos.tipoUsuario', // tipo de usuario asociado - ], - - }); - - console.log('Usuarios encontrados:', usuariosTipo); - - console.log('Usuarios encontrados:', usuarios); - - - - const header = [ 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', - 'carreras', 'rfc' + 'carreras', 'rfc', 'tipos_usuario' ].join(',') + '\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 + .join(' | '); // une todas las carreras en un solo string + + const tiposUsuario = (u.usuarioTipos ?? []) + .map(utu => utu.tipoUsuario?.tipo_usuario ?? '') + .join(' | '); // une todos los tipos de usuario en un solo string return [ u.num_cuenta ?? '', @@ -531,6 +520,7 @@ export class ExcelService { u.a_materno ?? '', carreras ?? '', u.rfc ?? '', + tiposUsuario ].join(','); }).join('\n'); @@ -573,6 +563,7 @@ export class ExcelService { 'genero', 'carreraUsuarios', // relación intermedia 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia + ], }); @@ -628,6 +619,8 @@ export class ExcelService { 'genero', 'carreraUsuarios', // relación intermedia 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia + 'usuarioTipos', + 'usuarioTipos.tipoUsuario', ], }); @@ -637,13 +630,17 @@ export class ExcelService { const header = [ 'num_cuenta', 'nombre', 'a_paterno', 'a_materno', - 'carreras', 'fecha_nacimiento', 'generacion' + 'carreras', 'fecha_nacimiento', 'generacion', 'tipos_usuario' ].join(',') + '\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 + .join(' | '); // une todas las carreras en un solo string + + const tiposUsuario = (u.usuarioTipos ?? []) + .map(utu => utu.tipoUsuario?.tipo_usuario ?? '') + .join(' | '); // une todos los tipos de usuario en un solo string return [ u.num_cuenta ?? '', @@ -652,7 +649,8 @@ export class ExcelService { u.a_materno ?? '', carreras ?? '', u.fecha_nacimiento ?? '', - u.generacion?.toString() ?? '' + u.generacion?.toString() ?? '', + tiposUsuario ].join(','); }).join('\n'); diff --git a/src/usuarios/usuarios.service.ts b/src/usuarios/usuarios.service.ts index 996e270..9d6583d 100644 --- a/src/usuarios/usuarios.service.ts +++ b/src/usuarios/usuarios.service.ts @@ -106,7 +106,7 @@ export class UsuariosService { - return { resultado, usuario: usuario.num_cuenta }; + return { resultado, usuario: usuario.nombre }; }