diff --git a/package.json b/package.json index 8782836..12187a9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@nestjs/common": "^8.0.0", "@nestjs/config": "^1.0.1", "@nestjs/core": "^8.0.0", + "@nestjs/jwt": "^8.0.0", "@nestjs/mapped-types": "*", "@nestjs/passport": "^8.0.1", "@nestjs/platform-express": "^8.0.0", @@ -53,6 +54,7 @@ "@types/express": "^4.17.13", "@types/jest": "^27.0.1", "@types/node": "^16.0.0", + "@types/passport-jwt": "^3.0.6", "@types/passport-local": "^1.0.34", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^4.28.2", diff --git a/src/app.module.ts b/src/app.module.ts index ce2cf6a..22dd095 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,7 +8,6 @@ import { LoggerModule } from 'nestjs-pino'; import { TransactionModule } from './transaction/transaction.module'; import { ProductModule } from './product/product.module'; import { ConfigurableModule } from './configurable/configurable.module'; -// import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module'; import configuration from './config/configuration'; @@ -22,10 +21,11 @@ import configuration from './config/configuration'; .valid('development', 'production', 'test', 'provision') .default('development'), PORT: Joi.number().default(3000), - DATABASE_CLIENT: Joi.valid('mysql', 'postgres'), - DATABASE_HOST: Joi.string(), - DATABASE_NAME: Joi.string(), - DATABASE_USERNAME: Joi.string(), + SECRET: Joi.string().required(), + DATABASE_CLIENT: Joi.valid('mysql', 'postgres').required(), + DATABASE_HOST: Joi.string().required(), + DATABASE_NAME: Joi.string().required(), + DATABASE_USERNAME: Joi.string().required(), DATABASE_PASSWORD: Joi.string().empty('').default(''), DATABASE_PORT: Joi.number().default(5432), }), @@ -53,6 +53,7 @@ import configuration from './config/configuration'; TransactionModule, ConfigurableModule, ProductModule, + AuthModule, ], }) export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index ea04269..b3eb1bb 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,13 +1,26 @@ -import { Controller, Post } from '@nestjs/common'; -import { InputLoginDto } from './dto/input-login.dto'; +import { Controller, Post, UseGuards, Request, Get } from '@nestjs/common'; +import { LocalAuthGuard } from './local-auth.guard'; import { AuthService } from './auth.service'; +import { JwtAuthGuard } from './jwt-auth.guard'; +import {Public} from "./public.decorator"; -@Controller('auth') +@Controller({ + path: 'auth', + version: '1', +}) export class AuthController { - constructor(private readonly authService: AuthService) {} + constructor(private authService: AuthService) {} - // @Post('login') - // public async login( @Body() loginUserDto: InputLoginDto): Promise { - // return await this.authService.findByLogin(loginUserDto); - // } + @Public() + @UseGuards(LocalAuthGuard) + @Post('login') + async login(@Request() req) { + return this.authService.login(req.user); + } + + @UseGuards(JwtAuthGuard) + @Get('profile') + getProfile(@Request() req) { + return req.user; + } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 75cbc4f..4ffd567 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -2,20 +2,40 @@ import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { UsersModule } from '../users/users.module'; import { PassportModule } from '@nestjs/passport'; -import { JwtModule, JwtStrategy } from 'passport-jwt'; +import { LocalStrategy } from './local.strategy'; import { AuthController } from './auth.controller'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtStrategy } from './jwt.strategy'; +import { APP_GUARD } from '@nestjs/core'; +import { JwtAuthGuard } from './jwt-auth.guard'; @Module({ imports: [ UsersModule, - PassportModule.register({ - defaultStrategy: 'jwt', - property: 'user', - session: false, + PassportModule, + ConfigModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => { + return { + secret: configService.get('secret'), + signOptions: { expiresIn: '1d' }, + }; + }, }), ], + providers: [ + AuthService, + LocalStrategy, + JwtStrategy, + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + ], controllers: [AuthController], - providers: [AuthService, JwtStrategy], - exports: [PassportModule, JwtModule], + exports: [AuthService], }) export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 1216af8..5e6ab4b 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,46 +1,35 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { UsersService } from '../users/users.service'; -import { InputLoginDto } from './dto/input-login.dto'; -import { InjectRepository } from '@nestjs/typeorm'; -import { User } from '../users/entities/user.entity'; -import { Repository } from 'typeorm'; import { hashPassword } from '../helper/hash_password'; -import { ResponseLoginDto } from './dto/response-login.dto'; +import { JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { constructor( - @InjectRepository(User) - private usersRepository: Repository, + private usersService: UsersService, + private jwtService: JwtService, ) {} - // async findByLogin({ username, password }: InputLoginDto): Promise { - // const user = await this.usersRepository.findOne({ where: { username } }); - // - // if (!user) { - // throw new HttpException( - // { - // statusCode: HttpStatus.FORBIDDEN, - // error: 'Username not found', - // }, - // HttpStatus.FORBIDDEN, - // ); - // } - // - // // compare passwords - // const hashData = await hashPassword(password, user.salt); - // - // if( hashData != user.password ){ - // throw new HttpException( - // { - // statusCode: HttpStatus.FORBIDDEN, - // error: 'Password Not Match', - // }, - // HttpStatus.FORBIDDEN, - // ); - // } - // - // return ResponseLoginDto(user); - // } + async validateUser(username: string, pass: string): Promise { + const user = await this.usersService.findOneByUsername(username); + if (user && user.password === (await hashPassword(pass, user.salt))) { + const { password, ...result } = user; + + return result; + } + + return null; + } + + async login(user: any) { + const payload = { + username: user.username, + sub: user.userId, + }; + + return { + access_token: this.jwtService.sign(payload), + }; + } } diff --git a/src/auth/dto/input-login.dto.ts b/src/auth/dto/input-login.dto.ts deleted file mode 100644 index 813ba07..0000000 --- a/src/auth/dto/input-login.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsNotEmpty } from 'class-validator'; - -export class InputLoginDto { - @IsNotEmpty() - username: string; - - @IsNotEmpty() - password: string; -} diff --git a/src/auth/dto/response-login.dto.ts b/src/auth/dto/response-login.dto.ts deleted file mode 100644 index 3132636..0000000 --- a/src/auth/dto/response-login.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsNotEmpty } from 'class-validator'; - -export class ResponseLoginDto { - @IsNotEmpty() - username: string; - - @IsNotEmpty() - jwt: string; -} diff --git a/src/auth/jwt-auth.guard.ts b/src/auth/jwt-auth.guard.ts new file mode 100644 index 0000000..9388eae --- /dev/null +++ b/src/auth/jwt-auth.guard.ts @@ -0,0 +1,24 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { Reflector } from '@nestjs/core'; +import { IS_PUBLIC_KEY } from './public.decorator'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (isPublic) { + return true; + } + + return super.canActivate(context); + } +} diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts new file mode 100644 index 0000000..08eccd1 --- /dev/null +++ b/src/auth/jwt.strategy.ts @@ -0,0 +1,19 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import {ConfigService} from "@nestjs/config"; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('secret'), + }); + } + + async validate(payload: any) { + return { userId: payload.sub, username: payload.username }; + } +} diff --git a/src/auth/local.strategy.ts b/src/auth/local.strategy.ts index 83541f4..b43a0aa 100644 --- a/src/auth/local.strategy.ts +++ b/src/auth/local.strategy.ts @@ -2,7 +2,6 @@ import { Strategy } from 'passport-local'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; -import { User } from '../users/entities/user.entity'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { @@ -10,19 +9,13 @@ export class LocalStrategy extends PassportStrategy(Strategy) { super(); } - // async validate( - // username: string, - // password: string, - // ): Promise> { - // const user = await this.authService.validateUser({ - // username, - // password, - // }); - // - // if (!user) { - // throw new UnauthorizedException(); - // } - // - // return user; - // } + async validate(username: string, password: string): Promise { + const user = await this.authService.validateUser(username, password); + + if (!user) { + throw new UnauthorizedException(); + } + + return user; + } } diff --git a/src/auth/public.decorator.ts b/src/auth/public.decorator.ts new file mode 100644 index 0000000..b3845e1 --- /dev/null +++ b/src/auth/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 17bbed2..1b088c3 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -1,6 +1,7 @@ export default () => { return { port: parseInt(process.env.PORT, 10) || 3000, + secret: process.env.SECRET, database: { client: process.env.DATABASE_CLIENT, host: process.env.DATABASE_HOST, diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index 2147aed..0f42d58 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -1,4 +1,4 @@ -import { IsNotEmpty, IsUUID } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsUUID, ValidateIf } from 'class-validator'; export class CreateUserDto { @IsNotEmpty() @@ -10,6 +10,9 @@ export class CreateUserDto { @IsUUID() roleId: string; + @ValidateIf((o) => { + return !!o.superior; + }) @IsUUID() superior: string; } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index f3389a6..3bd249e 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -13,6 +13,7 @@ import { import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; +import {Public} from "../auth/public.decorator"; @Controller({ path: 'users', @@ -32,6 +33,7 @@ export class UsersController { }; } + @Public() @Get() async findAll(@Query('page') page: number) { const [data, count] = await this.usersService.findAll(page); diff --git a/yarn.lock b/yarn.lock index 5581f74..64ac2a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -675,6 +675,14 @@ tslib "2.3.1" uuid "8.3.2" +"@nestjs/jwt@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-8.0.0.tgz#6c811c17634252dd1dcd5dabf409dbd692b812da" + integrity sha512-fz2LQgYY2zmuD8S+8UE215anwKyXlnB/1FwJQLVR47clNfMeFMK8WCxmn6xdPhF5JKuV1crO6FVabb1qWzDxqQ== + dependencies: + "@types/jsonwebtoken" "8.5.4" + jsonwebtoken "8.5.1" + "@nestjs/mapped-types@*": version "1.0.0" resolved "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.0.tgz" @@ -963,6 +971,20 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jsonwebtoken@*": + version "8.5.6" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.6.tgz#1913e5a61e70a192c5a444623da4901a7b1a9d42" + integrity sha512-+P3O/xC7nzVizIi5VbF34YtqSonFsdnbXBnWUCYRiKOi1f9gA4sEFvXkrGr/QVV23IbMYvcoerI7nnhDUiWXRQ== + dependencies: + "@types/node" "*" + +"@types/jsonwebtoken@8.5.4": + version "8.5.4" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz#50ccaf0aa6f5d7b9956e70fe323b76e582991913" + integrity sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg== + dependencies: + "@types/node" "*" + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -978,6 +1000,15 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/passport-jwt@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-3.0.6.tgz#41cc8b5803d5f5f06eb33e19c453b42716def4f1" + integrity sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ== + dependencies: + "@types/express" "*" + "@types/jsonwebtoken" "*" + "@types/passport-strategy" "*" + "@types/passport-local@^1.0.34": version "1.0.34" resolved "https://registry.yarnpkg.com/@types/passport-local/-/passport-local-1.0.34.tgz#84d3b35b2fd4d36295039ded17fe5f3eaa62f4f6" @@ -1504,7 +1535,7 @@ avvio@^7.1.2: fastq "^1.6.1" queue-microtask "^1.1.2" -axios@0.24.0: +axios@0.24.0, axios@^0.24.0: version "0.24.0" resolved "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz" integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== @@ -3788,7 +3819,7 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonwebtoken@^8.2.0: +jsonwebtoken@8.5.1, jsonwebtoken@^8.2.0: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==