diff --git a/package-lock.json b/package-lock.json index f904edc..651ffb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,16 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "argon2": "^0.41.1", "dotenv": "^16.4.7", "lodash": "^4.17.21", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "reflect-metadata": "^0.2.0", @@ -31,6 +36,8 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", "@types/sequelize": "^4.28.20", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", @@ -1711,6 +1718,19 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz", @@ -1731,6 +1751,16 @@ } } }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "10.4.15", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.15.tgz", @@ -2164,6 +2194,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.17.14", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz", @@ -2200,6 +2239,50 @@ "undici-types": "~6.19.2" } }, + "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-local": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", + "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@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.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", @@ -3272,6 +3355,12 @@ "ieee754": "^1.1.13" } }, + "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", @@ -4037,6 +4126,15 @@ "dev": true, "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", @@ -6515,6 +6613,49 @@ "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/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "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", @@ -6598,6 +6739,42 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "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", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "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.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.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6612,6 +6789,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/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -7237,6 +7420,53 @@ "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-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.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", @@ -7313,6 +7543,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/pg": { "version": "8.13.1", "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", diff --git a/package.json b/package.json index 95ac647..7ba8e07 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,16 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "argon2": "^0.41.1", "dotenv": "^16.4.7", "lodash": "^4.17.21", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "reflect-metadata": "^0.2.0", @@ -42,6 +47,8 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", "@types/sequelize": "^4.28.20", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", diff --git a/src/app.module.ts b/src/app.module.ts index 92956ea..61bef9a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { ConfigModule } from '@nestjs/config'; import { UsersModule } from './modules/users/users.module'; import { UsersController } from './modules/users/users.controller'; import { ProductsModule } from './modules/products/products.module'; +import { AuthModule } from './modules/auth/auth.module'; @Module({ imports: [ @@ -13,6 +14,7 @@ import { ProductsModule } from './modules/products/products.module'; ConfigModule.forRoot({ isGlobal: true }), UsersModule, ProductsModule, + AuthModule, ], controllers: [AppController, UsersController], providers: [AppService], diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..690f317 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Post, Body, Request, UseGuards } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { CreateUserDto } from '../users/dto'; +import { AuthGuard } from '@nestjs/passport'; + +@Controller('auth') +export class AuthController { + constructor(private authService: AuthService) {} + + @UseGuards(AuthGuard('local')) + @Post('login') + async login(@Request() req) { + return await this.authService.login(req.user); + } + + @Post('signup') + async signUp(@Body() createUserDto: CreateUserDto) { + return await this.authService.create(createUserDto); + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..f73135e --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { PassportModule } from '@nestjs/passport'; +import { LocalStrategy } from './local.strategy'; +import { UsersModule } from '../users/users.module'; +import { JwtModule } from '@nestjs/jwt'; +import { JwtStrategy } from './jwt.strategy'; + +@Module({ + imports: [ + PassportModule, + UsersModule, + JwtModule.register({ + secret: process.env.JWTKEY, + signOptions: { expiresIn: process.env.TOKEN_EXPIRATION }, + }), + ], + controllers: [AuthController], + providers: [AuthService, LocalStrategy, JwtStrategy], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..3b350ca --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { UsersService } from '../users/users.service'; +import * as argon from 'argon2'; +import { JwtService } from '@nestjs/jwt'; + +@Injectable() +export class AuthService { + constructor( + private readonly userService: UsersService, + private readonly jwtService: JwtService, + ) {} + + async validateUser(username: string, pass: string) { + // find if user exist with this email + const user = await this.userService.findOneByEmail(username); + if (!user) { + return null; + } + + const match = await this.comparePassword(pass, user.password); + if (!match) { + return null; + } + + const { password, ...result } = user['dataValues']; + return result; + } + + public async login(user) { + const token = await this.generateToken(user); + return { user, token }; + } + + public async create(user) { + const createdUser = await this.userService.create(user); + const token = await this.generateToken(createdUser); + + return { user: createdUser, token }; + } + + private async generateToken(user) { + const token = await this.jwtService.signAsync(user); + return token; + } + + private async hashPassword(password) { + const hash = await argon.hash(password); + return hash; + } + + private async comparePassword(enteredPassword, dbPassword) { + const match = await argon.verify(dbPassword, enteredPassword); + return match; + } +} diff --git a/src/modules/auth/jwt.strategy.ts b/src/modules/auth/jwt.strategy.ts new file mode 100644 index 0000000..52ae537 --- /dev/null +++ b/src/modules/auth/jwt.strategy.ts @@ -0,0 +1,25 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { UsersService } from '../users/users.service'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(private readonly userService: UsersService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: process.env.JWTKEY, + }); + } + + async validate(payload: any) { + const user = await this.userService.findOne(payload.id); + if (!user) { + throw new UnauthorizedException( + 'You are not authorized to perform the operation', + ); + } + return payload; + } +} diff --git a/src/modules/auth/local.strategy.ts b/src/modules/auth/local.strategy.ts new file mode 100644 index 0000000..267b263 --- /dev/null +++ b/src/modules/auth/local.strategy.ts @@ -0,0 +1,20 @@ +import { Strategy } from 'passport-local'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { AuthService } from './auth.service'; + +@Injectable() +export class LocalStrategy extends PassportStrategy(Strategy) { + constructor(private readonly authService: AuthService) { + super(); + } + + async validate(username: string, password: string): Promise { + const user = await this.authService.validateUser(username, password); + + if (!user) { + throw new UnauthorizedException('Invalid user credentials'); + } + return user; + } +} diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 2f5a124..23ca00f 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -36,15 +36,6 @@ export class UsersService { ]); } - // async update(id: UUID, updateUserDto: UpdateUserDto) { - // const [numberOfAffectedRows, [updatedUser]] = - // await this.userRepository.update( - // { ...updateUserDto }, - // { where: { uuid: id }, returning: true }, - // ); - // return { numberOfAffectedRows, updatedUser }; - // } - async update(id: UUID, updateUserDto: UpdateUserDto) { const [numberOfAffectedRows, [updatedUser]] = await this.userRepository.update( @@ -61,4 +52,8 @@ export class UsersService { return deletedUser; } + + async findOneByEmail(email: string): Promise { + return await this.userRepository.findOne({ where: { email } }); + } }