diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 083ccc6..d2af33a 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -1,10 +1,14 @@ -version: '3.9' services: database: image: mariadb:latest container_name: carga_db restart: always + environment: + - MARIADB_ROOT_PASSWORD=root + - MARIADB_DATABASE=carga_test + - MARIADB_USER=miusuario + - MARIADB_PASSWORD=root env_file: - ../.env ports: diff --git a/package-lock.json b/package-lock.json index 68cf1dd..98aec47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,10 +27,11 @@ "mysql2": "^3.14.1", "nodemailer": "^7.0.3", "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "typeorm": "^0.3.24" + "typeorm": "^0.3.26" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -913,19 +914,30 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "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", @@ -1081,14 +1093,13 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.13", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", - "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", "dev": true, - "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", @@ -1109,15 +1120,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.13.tgz", - "integrity": "sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==", + "version": "4.2.17", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.17.tgz", + "integrity": "sha512-r6bQLsyPSzbWrZZ9ufoWL+CztkSatnJ6uSxqd6N+o41EZC51sQeWOzI6s5jLb+xxTWxl7PlUppqm8/sow241gg==", "dev": true, - "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.13", - "@inquirer/type": "^3.0.7", - "external-editor": "^3.1.0" + "@inquirer/core": "^10.1.15", + "@inquirer/external-editor": "^1.0.1", + "@inquirer/type": "^3.0.8" }, "engines": { "node": ">=18" @@ -1154,12 +1164,32 @@ } } }, - "node_modules/@inquirer/figures": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", - "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "node_modules/@inquirer/external-editor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz", + "integrity": "sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==", + "dev": true, + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.6.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" } @@ -1334,11 +1364,10 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", - "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -2632,21 +2661,19 @@ "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", - "integrity": "sha512-hEDNMlaPiBO72fxxX/CuRQL3MEhKRc/sIYGVoXjrnw6hTxZdezvvM6A95UaLsYknfmcZZa/CdG1SMBZOu9agHQ==", - "license": "MIT", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.6.tgz", + "integrity": "sha512-HErwPmKnk+loTq8qzu1up+k7FC6Kqa8x6lJ4cDw77KnTxLzsCaPt+jBvOq6UfICmfqcqCCf3dKXg+aObQp+kIQ==", "dependencies": { "cors": "2.8.5", "express": "5.1.0", - "multer": "2.0.1", + "multer": "2.0.2", "path-to-regexp": "8.2.0", "tslib": "2.8.1" }, @@ -2835,7 +2862,6 @@ "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", @@ -5124,6 +5150,20 @@ "dev": true, "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/aws-ssl-profiles": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", @@ -5300,6 +5340,14 @@ ], "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -5598,6 +5646,23 @@ "node": ">=14.16" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -5708,11 +5773,10 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true, - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "dev": true }, "node_modules/chokidar": { "version": "4.0.3", @@ -6312,6 +6376,22 @@ "node": ">=10" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6902,15 +6982,6 @@ "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", @@ -7058,34 +7129,6 @@ "node": ">=4" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "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", @@ -7390,6 +7433,20 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -7435,11 +7492,10 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, - "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7836,6 +7892,17 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -7852,7 +7919,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -8077,6 +8143,17 @@ "dev": true, "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -8190,6 +8267,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -9793,10 +9884,9 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", - "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", - "license": "MIT", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", @@ -10022,6 +10112,11 @@ "node": ">=8" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10145,16 +10240,6 @@ "node": ">=8" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", @@ -10278,6 +10363,17 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/passport-jwt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", @@ -10288,6 +10384,25 @@ "passport-strategy": "^1.0.0" } }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -10522,6 +10637,14 @@ "node": ">=4" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11208,6 +11331,22 @@ "node": ">= 18" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -11221,16 +11360,22 @@ "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)", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shebang-command": { @@ -12052,16 +12197,11 @@ "license": "MIT" }, "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "engines": { - "node": ">=0.6.0" + "node": ">=14.14" } }, "node_modules/tmpl": { @@ -12071,6 +12211,24 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -12367,6 +12525,19 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -12374,10 +12545,9 @@ "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", + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz", + "integrity": "sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==", "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", @@ -12407,9 +12577,8 @@ }, "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", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", "ioredis": "^5.0.4", "mongodb": "^5.8.0 || ^6.0.0", "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", @@ -12418,7 +12587,7 @@ "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", - "redis": "^3.1.1 || ^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", "reflect-metadata": "^0.1.14 || ^0.2.0", "sql.js": "^1.4.0", "sqlite3": "^5.0.3", @@ -12435,9 +12604,6 @@ "better-sqlite3": { "optional": true }, - "hdb-pool": { - "optional": true - }, "ioredis": { "optional": true }, @@ -12633,6 +12799,11 @@ "node": ">=8" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "node_modules/uint8array-extras": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", @@ -13100,6 +13271,26 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index d9cf5de..7920a72 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,11 @@ "mysql2": "^3.14.1", "nodemailer": "^7.0.3", "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "typeorm": "^0.3.24" + "typeorm": "^0.3.26" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index a49b9de..0439d8f 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,10 +1,13 @@ import { AuthDocumentation } from './auth.documentation'; -import { Controller, Post, Body, UseGuards, Request, Get } from '@nestjs/common'; +import { Controller, Post, Body, UseGuards, Request, Get, Req, Res } 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'; import { ApiBearerAuth } from '@nestjs/swagger'; +import { SetPasswordDto } from './dto/createPassword.dto'; +import { Response } from 'express'; +import { WhiteDto } from './dto/whiteList.dto'; @Controller() export class AuthController { @@ -34,6 +37,8 @@ export class AuthController { }; } + + @@ -43,6 +48,73 @@ export class AuthController { return this.authService.register(dto); } + @Post('white-list') + @AuthDocumentation.white() + async newRegister(@Body() dto:WhiteDto){ + + return this.authService.white(dto) + + } + + + //Inicio de sesión con GOOGLE + + @Get('google') + @UseGuards(AuthGuard('google')) + async googleLogin() { + return { msg: 'Redirigiendo a Google' }; + } + + // Callback de Google + // @Get('google/callback') + // @UseGuards(AuthGuard('google')) + // async googleCallback(@Req() req) { + // const user = req.user; + + // if (user.needsPassword) { + // return { + // status: 'NEEDS_PASSWORD', + // email: user.correo, + // message: 'El usuario debe crear una contraseña antes de continuar', + // }; + // } + + // return this.authService.login(user); // genera JWT + // } + + // auth.controller.ts + @Get('google/callback') + @UseGuards(AuthGuard('google')) + async googleCallback(@Req() req, @Res() res: Response) { + const user = req.user; + + if (user.needsPassword) { + + return res.redirect( + `${process.env.FRONTEND_URL}/password?email=${user.correo}`, + ); + } + + const jwt = await this.authService.login(user); + + + return res.redirect( + `${process.env.FRONTEND_URL}/oauth-callback?token=${jwt.access_token}`, + ); + } + + + //Inicio de sesión con GOOGLE + + //Set-contraseña + // auth.controller.ts + + + @Post('set-password') + async setPassword(@Body() dto: SetPasswordDto) { + return this.authService.setPassword(dto); + } + } diff --git a/src/auth/auth.documentation.ts b/src/auth/auth.documentation.ts index 537a7f6..f28b1c9 100644 --- a/src/auth/auth.documentation.ts +++ b/src/auth/auth.documentation.ts @@ -101,4 +101,40 @@ export class AuthDocumentation { ); } + static white() { + return applyDecorators( + ApiTags('Auth'), + ApiOperation({ + summary: 'Registrar usuario', + description: 'Crea un nuevo usuario con correo y origen para una white list.' + }), + ApiConsumes('application/json'), + ApiBody({ + schema: { + type: 'object', + properties: { + email: { type: 'string', format: 'email', example: 'nuevo@dominio.com' }, + origen: { type: 'string', example: 'RED', description: 'Origen del usuario' } + + } + } + }), + 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: 'RED' } + + + } + } + }), + ApiResponse({ status: 409, description: 'El correo ya está registrado.' }), + ); + } + } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 2b406e0..802208b 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -8,6 +8,7 @@ import { AuthController } from './auth.controller'; import { JwtStrategy } from './jwt.strategy'; import { Origen, UsuariosDelSistema } from '../entities/entities'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { GoogleStrategy } from './google.strategy'; @Module({ imports: [ @@ -22,7 +23,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; }), }), ], - providers: [AuthService, JwtStrategy], + providers: [AuthService, JwtStrategy, GoogleStrategy], controllers: [AuthController], }) export class AuthModule { } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 0bacb35..037ca12 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -6,6 +6,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Origen, UsuariosDelSistema } from '../entities/entities'; import * as bcrypt from 'bcrypt'; import { RegisterDto } from './dto/register.dto'; +import { SetPasswordDto } from './dto/createPassword.dto'; +import { WhiteDto } from './dto/whiteList.dto'; @Injectable() export class AuthService { @@ -75,4 +77,92 @@ export class AuthService { const { contraseña, ...result } = saved; return result; } + + + async white(dto: WhiteDto) { + // 1) Verificar duplicado + const exists = await this.userRepo.findOne({ where: { correo: dto.email } }); + if (exists) { + throw new ConflictException('El correo ya está registrado'); + } + + + + 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, + origen: origen, + + }); + + // 4) Guardar en BD + const saved = await this.userRepo.save(user); + + // 5) Devolver sin contraseña + const { contraseña, ...result } = saved; + return result; + } + + + async validateGoogleUser(email: string) { + const user = await this.userRepo.findOne({ where: { correo: email }, relations: ['origen'] }); + + if (!user) { + // No permitir crear cuenta + return null; + } + + if (!user.contraseña) { + // Usuario existe pero nunca puso contraseña → permitirle definirla + return { + ...user, + needsPassword: true, + }; + } + + // Usuario con contraseña → login normal + return user; +} + +//Poner contraseñas +// auth.service.ts +async setPassword(dto: SetPasswordDto) { + const user = await this.userRepo.findOne({ + where: { correo: dto.email }, + relations: ['origen'], + }); + + if (!user) { + throw new UnauthorizedException('Usuario no encontrado'); + } + + if (user.contraseña) { + throw new ConflictException('Este usuario ya tiene contraseña'); + } + + // Hash contraseña nueva + const hash = await bcrypt.hash(dto.password, 10); + + user.contraseña = hash; + const updated = await this.userRepo.save(user); + + // Retornar con JWT inmediato (login automático) + const payload = { sub: updated.id_usuario, email: updated.correo, tipo: updated.origen.origen }; + + return { + access_token: this.jwtService.sign(payload), + }; +} + + + + } diff --git a/src/auth/dto/createPassword.dto.ts b/src/auth/dto/createPassword.dto.ts new file mode 100644 index 0000000..16a4fd9 --- /dev/null +++ b/src/auth/dto/createPassword.dto.ts @@ -0,0 +1,11 @@ +// dto/set-password.dto.ts +import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; + +export class SetPasswordDto { + @IsEmail() + email: string; + + @IsNotEmpty() + @MinLength(6) + password: string; +} diff --git a/src/auth/dto/whiteList.dto.ts b/src/auth/dto/whiteList.dto.ts new file mode 100644 index 0000000..91612b1 --- /dev/null +++ b/src/auth/dto/whiteList.dto.ts @@ -0,0 +1,10 @@ +import { IsEmail, IsString, MinLength, MaxLength, IsOptional, IsNumber } from 'class-validator'; + +export class WhiteDto { + @IsEmail() + email: string; + + @IsString() + @MaxLength(60) + origen: string; +} \ No newline at end of file diff --git a/src/auth/google.strategy.ts b/src/auth/google.strategy.ts new file mode 100644 index 0000000..47c079c --- /dev/null +++ b/src/auth/google.strategy.ts @@ -0,0 +1,39 @@ +// google.strategy.ts +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { Strategy, VerifyCallback } from 'passport-google-oauth20'; +import { ConfigService } from '@nestjs/config'; +import { AuthService } from './auth.service'; + +@Injectable() +export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { + constructor( + private readonly authService: AuthService, + private readonly config: ConfigService, + ) { + super({ + clientID: config.get('GOOGLE_CLIENT_ID'), + clientSecret: config.get('GOOGLE_CLIENT_SECRET'), + callbackURL: config.get('GOOGLE_CALLBACK_URL'), + scope: ['email', 'profile'], + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: any, + done: VerifyCallback, + ): Promise { + const { emails } = profile; + const email = emails[0].value; + + const user = await this.authService.validateGoogleUser(email); + + if (!user) { + throw new UnauthorizedException('Usuario no registrado en el sistema'); + } + + done(null, user); + } +} diff --git a/src/entities/entities.ts b/src/entities/entities.ts index d10df47..57202dc 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -196,7 +196,7 @@ export class TipoUsuario { @PrimaryGeneratedColumn({ name: 'id_tipo_usuario', type: 'int' }) id_tipo_usuario: number; - @Column({ name: 'tipo_usuario', type: 'varchar', length: 20 }) + @Column({ name: 'tipo_usuario', type: 'varchar', length: 30 }) tipo_usuario: string; @OneToMany(() => UsuarioTipoUsuario, utu => utu.tipoUsuario) diff --git a/src/excel/excel.controller.ts b/src/excel/excel.controller.ts index 3ad935c..928f7c3 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, Body, BadRequestException } from '@nestjs/common'; +import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus, Param, ParseIntPipe, Body, BadRequestException, Delete } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express'; @@ -69,6 +69,7 @@ export class ExcelController { @ExcelDocumentation.loadExcel() async loadExcel(@Request() req, @UploadedFile() file: Express.Multer.File) { const origen = req.user.origen; // ahora sí existe + if (origen != 'LOAD') { await this.movimientoService.log( origen, @@ -78,6 +79,7 @@ export class ExcelController { throw new Error('Origen no permitido para carga.'); } try { + const result = await this.excelService.loadFile(file.buffer); //Actualizar el estado de los movimientos if (!result) { @@ -120,6 +122,24 @@ export class ExcelController { }); await this.excelService.enviarCargaMasiva(); + + + await this.excelService.enviarInforme('AT',"Carga de datos en Servicios PCpuma", + `Se ha hecho una carga en el sistema \n ${result.conteoTiposAt}` + ) + + await this.excelService.enviarInforme('RED',"Carga de datos en Servicios PCpuma", + `Se ha hecho una carga en el sistema \n ${result.conteoTiposRed}` + ) + + await this.excelService.enviarInforme('SOLICITA',"Carga de datos en Servicios PCpuma", + `Se ha hecho una carga en el sistema \n ${result.conteoTiposCorreo}` + ) + + await this.excelService.enviarInforme('CORREO',"Carga de datos en Servicios PCpuma", + `Se ha hecho una carga en el sistema \n ${result.conteoTiposCorreo}` + ) + return result @@ -300,6 +320,11 @@ export class ExcelController { } + // @Delete() + // async borrar(){ + // return await this.excelService.borrarUsers() + // } + diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index ea15dbf..96ed89e 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -198,178 +198,244 @@ export class ExcelService { * Valida y luego persiste usuarios y relaciones básicas */ async loadFile(buffer: Buffer) { - const file = await this.parseFile(buffer); - const status = this.validateRows(file); - const errs = status.errors; - const rows = status.rowsGood; + const file = await this.parseFile(buffer); + const status = this.validateRows(file); + const errs = status.errors; + const rows = status.rowsGood; - if (rows.length === 0) { - throw new BadRequestException('No se encontraron filas válidas para cargar. ' + errs); - } + 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 conteoTiposRed: Record = {}; + const conteoTiposAt: Record = {}; + const conteoTiposSolicita: Record = {}; + const conteoTiposCorreo: Record = {}; - try { - const movimiento = await this.movimientoService.logger( - queryRunner, - 'LOAD', - 'LOADING', - undefined, - 'CARGA MASIVA DE USUARIOS' - ); + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); - let contador = 0; + try { + const movimiento = await this.movimientoService.logger( + queryRunner, + 'LOAD', + 'LOADING', + undefined, + 'CARGA MASIVA DE USUARIOS', + ); - for (const r of rows) { + // 🔹 1. Pre-cargar cuentas/rfc ya existentes + const cuentas = rows.map((r) => r.cuenta).filter(Boolean); + const rfcs = rows.map((r) => r.rfc).filter(Boolean); - const whereCond: Array<{ num_cuenta?: string; rfc?: string }> = [{ num_cuenta: r.cuenta }]; - if (r.rfc) { - whereCond.push({ rfc: r.rfc }); - } + const existentes = await queryRunner.manager.find(Usuario, { + where: [{ num_cuenta: In(cuentas) }, { rfc: In(rfcs) }], + select: ['num_cuenta', 'rfc'], + }); - const userExists = await queryRunner.manager.findOne(Usuario, { - where: whereCond, - }); + const setCuentas = new Set(existentes.map((u) => u.num_cuenta)); + const setRfcs = new Set(existentes.map((u) => u.rfc)); + // 🔹 2. Pre-cargar catálogos + const generosUnicos = [...new Set(rows.map((r) => r.sexo).filter(Boolean))]; + const carrerasUnicasClave = [ + ...new Set(rows.map((r) => r.clave).filter(Boolean)), + ]; + const carrerasUnicasNombre = [ + ...new Set(rows.map((r) => r.nomCarr).filter(Boolean)), + ]; + const tiposUnicos = [...new Set(rows.map((r) => r.tipo?.trim()).filter(Boolean))]; + const [generosExist, carrerasExist, tiposExist] = await Promise.all([ + queryRunner.manager.find(Genero, { where: { genero: In(generosUnicos) } }), + queryRunner.manager.find(Carrera, { + where: [{ clave: In(carrerasUnicasClave) }, { carrera: In(carrerasUnicasNombre) }], + }), + queryRunner.manager.find(TipoUsuario, { + where: { tipo_usuario: In(tiposUnicos) }, + }), + ]); - if (userExists) { - errs.push(`Cuenta ${r.cuenta} ya existe, se omite.`); - contador++; - continue; - } + const mapGenero = new Map(generosExist.map((g) => [g.genero, g])); + const mapCarreraClave = new Map(carrerasExist.map((c) => [c.clave, c])); + const mapCarreraNom = new Map(carrerasExist.map((c) => [c.carrera, c])); + const mapTipo = new Map(tiposExist.map((t) => [t.tipo_usuario, t])); - if (!r.tipo || !r.nombres || !r.apellidopa || !r.apellidoma) { - errs.push(`Fila con cuenta ${r.cuenta} tiene campos faltantes.`); - contador++; - continue; - } + // 🔹 3. Arreglos para inserciones masivas + const usuarios: Usuario[] = []; + const carrerasUsuario: CarreraUsuario[] = []; + const usuariosTipos: UsuarioTipoUsuario[] = []; + const serviciosActivos: ServActivos[] = []; - 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 contador = 0; - let carrera - if (r.clave || r.nomCarr) { - if (!r.clave) { - carrera = await queryRunner.manager.findOne(Carrera, { where: { carrera: r.nomCarr } }); - - - } else if (!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() }, - }); - 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, - 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); + for (const r of rows) { + if (setCuentas.has(r.cuenta) || (r.rfc && setRfcs.has(r.rfc))) { + errs.push(`Cuenta ${r.cuenta} ya existe, se omite.`); + contador++; + continue; } - 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(); + if (!r.tipo || !r.nombres || !r.apellidopa || !r.apellidoma) { + errs.push(`Fila con cuenta ${r.cuenta} tiene campos faltantes.`); + contador++; + continue; + } + + // Genero + let genero = mapGenero.get(r.sexo); + if (!genero) { + genero = queryRunner.manager.create(Genero, { genero: r.sexo }); + mapGenero.set(r.sexo, genero); + } + + // Carrera + let carrera: Carrera|undefined; + if (r.clave && mapCarreraClave.has(r.clave)) { + carrera = mapCarreraClave.get(r.clave); + } else if (r.nomCarr && mapCarreraNom.has(r.nomCarr)) { + carrera = mapCarreraNom.get(r.nomCarr); + } else if (r.clave || r.nomCarr) { + carrera = queryRunner.manager.create(Carrera, { + clave: r.clave ?? '', + carrera: r.nomCarr?.slice(0, 100) ?? '', + }); + mapCarreraClave.set(carrera.clave, carrera); + mapCarreraNom.set(carrera.carrera, carrera); + } + + // Tipo + let tipo = mapTipo.get(r.tipo.trim()); + if (!tipo) { + tipo = queryRunner.manager.create(TipoUsuario, { + tipo_usuario: r.tipo.trim(), + }); + mapTipo.set(r.tipo.trim(), 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, + a_paterno: r.apellidopa, + a_materno: r.apellidoma, + rfc: r.rfc, + fecha_nacimiento: r.fechnac, + generacion, + genero, + movimiento, + }); + + usuarios.push(usuario); + + if (carrera) { + const carreraUsuario = queryRunner.manager.create(CarreraUsuario, { + usuario, + carrera, + }); + carrerasUsuario.push(carreraUsuario); + } + + usuariosTipos.push( + queryRunner.manager.create(UsuarioTipoUsuario, { + usuario, + tipoUsuario: tipo, + }), + ); + + // Servicios activos + const servData: Partial = { + usuario, + RedStatus: 'Inactivo', + ATStatus: 'Inactivo', + CorreoStatus: 'Inactivo', + PrestamosStatus: 'Inactivo', + }; + + switch (r.tipo.trim()) { + case 'Diplomado': + Object.assign(servData, { Correo: true }); + conteoTiposCorreo[r.tipo] = (conteoTiposCorreo[r.tipo] ?? 0) + 1; + break; + case 'Extra Largo': + Object.assign(servData, { Correo: true, Prestamos: true }); + conteoTiposCorreo[r.tipo] = (conteoTiposCorreo[r.tipo] ?? 0) + 1; + conteoTiposSolicita[r.tipo] = (conteoTiposSolicita[r.tipo] ?? 0) + 1; + break; + case 'Servicio Social': + Object.assign(servData, { Correo: true, AT: true, Red: true }); + conteoTiposCorreo[r.tipo] = (conteoTiposCorreo[r.tipo] ?? 0) + 1; + conteoTiposAt[r.tipo] = (conteoTiposAt[r.tipo] ?? 0) + 1; + conteoTiposRed[r.tipo] = (conteoTiposRed[r.tipo] ?? 0) + 1; + break; + case 'Idiomas R (UNAM)': + case 'Idiomas Sabatino': + Object.assign(servData, { Correo: true, Red: true }); + conteoTiposCorreo[r.tipo] = (conteoTiposCorreo[r.tipo] ?? 0) + 1; + conteoTiposRed[r.tipo] = (conteoTiposRed[r.tipo] ?? 0) + 1; + 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, + }); + conteoTiposCorreo[r.tipo] = (conteoTiposCorreo[r.tipo] ?? 0) + 1; + conteoTiposRed[r.tipo] = (conteoTiposRed[r.tipo] ?? 0) + 1; + conteoTiposAt[r.tipo] = (conteoTiposAt[r.tipo] ?? 0) + 1; + conteoTiposSolicita[r.tipo] = (conteoTiposSolicita[r.tipo] ?? 0) + 1; + break; + case 'Trabajadores': + Object.assign(servData, { Red: true }); + conteoTiposRed[r.tipo] = (conteoTiposRed[r.tipo] ?? 0) + 1; + break; + } + + serviciosActivos.push(queryRunner.manager.create(ServActivos, servData)); } + + // 🔹 4. Guardar en batch + await queryRunner.manager.save([...mapGenero.values()]); + await queryRunner.manager.save([...mapCarreraClave.values()]); + await queryRunner.manager.save([...mapTipo.values()]); + + await queryRunner.manager.save(usuarios); + await queryRunner.manager.save(carrerasUsuario); + await queryRunner.manager.save(usuariosTipos); + await queryRunner.manager.save(serviciosActivos); + + await queryRunner.commitTransaction(); + + return { + inserted: rows.length - contador, + id_movimiento: movimiento.id_mov, + errors: errs, + conteoTiposAt, + conteoTiposCorreo, + conteoTiposRed, + conteoTiposSolicita, + }; + } catch (err) { + await queryRunner.rollbackTransaction(); + throw err; + } finally { + await queryRunner.release(); } +} + @@ -571,13 +637,10 @@ export class ExcelService { }, servActivo: { RedStatus: 'Inactivo', - Red: true, // solo los que tienen el servicio de red activo + Red: true, // solo los que tienen el servicio de red } }, relations: [ - 'genero', - 'carreraUsuarios', // relación intermedia - 'carreraUsuarios.carrera', // carrera asociada a través de la intermedia 'usuarioTipos', 'usuarioTipos.tipoUsuario', ], @@ -737,7 +800,7 @@ export class ExcelService { throw new BadRequestException('No se encontraron usuarios para enviar correo'); } - // Aquí puedes enviar el correo usando MailService + // Aquí puedes enviar el correo usnado MailService await this.mailService.sendMail({ to: usuarios_red.correo ?? '', subject: 'Carga de Usuarios', @@ -756,12 +819,7 @@ export class ExcelService { 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: '', - }); + @@ -770,6 +828,24 @@ export class ExcelService { + async enviarInforme(origen:string, subject:string ,text:string, html:string = "" ):Promise{ + const user = await this.usuariosDelSistemaRepo.find({where:{origen:{origen}}}) + + user.forEach(async(u)=>{ + await this.mailService.sendMail({ + to: u.correo ?? '', + subject, + text, + html, + }); + + + }) + + + } + + diff --git a/src/main.ts b/src/main.ts index 5eeb055..aef20e8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,16 @@ +import * as crypto from "crypto"; + +// Hacer que esté disponible como global +(global as any).crypto = crypto; + + 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); app.enableCors({ exposedHeaders: ['Content-Disposition'] }); diff --git a/src/usuarios/usuarios.service.ts b/src/usuarios/usuarios.service.ts index 10af1ba..36087d5 100644 --- a/src/usuarios/usuarios.service.ts +++ b/src/usuarios/usuarios.service.ts @@ -460,11 +460,19 @@ export class UsuariosService { } } - 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); + let carrera; + if(usuario.carrera!=undefined){ + carrera = await queryRunner.manager.findOne(Carrera, { where: { carrera: usuario.carrera } }); + }else{ + carrera= await queryRunner.manager.findOne(Carrera, {where:{carrera: ""}}) + if(!carrera){ + const nuevaCarrera = queryRunner.manager.create(Carrera, { carrera: "", clave: "" }); + carrera = await queryRunner.manager.save(nuevaCarrera); + } + } + + const nuevo = queryRunner.manager.create(Usuario, { @@ -480,6 +488,8 @@ export class UsuariosService { contraseña: usuario.contraseña ?? null, // Asegúrate de manejar la contraseña adecuadamente }); + console.log(nuevo) + const savedUser = await queryRunner.manager.save(nuevo); const userCarrera = queryRunner.manager.create(CarreraUsuario, { carrera, usuario: savedUser }); @@ -622,6 +632,8 @@ export class UsuariosService { console.log(savedUser) await queryRunner.commitTransaction(); + + return { saved: savedUser, movId: mov.id_mov }; } catch (error) {