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[];