Skip to content

Commit

Permalink
Merge pull request #9 from huongnt-2545/part9_authentication
Browse files Browse the repository at this point in the history
Part9 authentication
  • Loading branch information
huongnt-2545 authored Sep 19, 2021
2 parents 419b213 + bd04498 commit 812222b
Show file tree
Hide file tree
Showing 17 changed files with 1,589 additions and 38 deletions.
1,009 changes: 980 additions & 29 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.0.1",
"@nestjs/core": "^8.0.0",
"@nestjs/jwt": "^8.0.0",
"@nestjs/passport": "^8.0.1",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/typeorm": "^8.0.2",
"bcrypt": "^5.0.1",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"passport": "^0.4.1",
"passport-jwt": "^4.0.0",
"passwort": "^1.0.4",
"pg": "^8.7.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
Expand All @@ -38,9 +44,11 @@
"@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0",
"@types/bcrypt": "^5.0.0",
"@types/express": "^4.17.13",
"@types/jest": "^26.0.24",
"@types/node": "^16.0.0",
"@types/passport-jwt": "^3.0.6",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { typeOrmConfig } from './config/typeorm.config';
// import configuration from './config/configuration';
import { TasksModule } from './tasks/tasks.module';
import { AuthModule } from './auth/auth.module';

@Module({
imports: [
Expand All @@ -13,6 +14,7 @@ import { TasksModule } from './tasks/tasks.module';
}),
TypeOrmModule.forRoot(typeOrmConfig),
TasksModule,
AuthModule,
],
})
export class AppModule {}
22 changes: 22 additions & 0 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthCredentialsDto } from './dto/auth-credentials.dto';
import { GetUser } from './get-user.decorator';
import { User } from './user.entity';

@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}

@Post('/signup')
signUp(@Body() authCredentialsDto: AuthCredentialsDto): Promise<void> {
return this.authService.createUser(authCredentialsDto);
}

@Post('/signin')
signIn(
@Body() authCredentialsDto: AuthCredentialsDto,
): Promise<{ accessToken: string }> {
return this.authService.validateUserPassword(authCredentialsDto);
}
}
30 changes: 30 additions & 0 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DatabaseModule } from '../database.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { userProviders } from './user.provider';
// import { UserRepository } from './user.repository';

@Module({
// imports: [TypeOrmModule.forFeature([UserRepository])],
imports: [
DatabaseModule,
PassportModule.register({
defaultStrategy: 'jwt',
}),
JwtModule.register({
secret: 'topSecret51',
signOptions: {
expiresIn: 3600,
},
}),
],
controllers: [AuthController],
providers: [...userProviders, AuthService, JwtStrategy],
exports: [JwtStrategy, PassportModule],
})
export class AuthModule {}
65 changes: 65 additions & 0 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
ConflictException,
Inject,
Injectable,
InternalServerErrorException,
UnauthorizedException,
} from '@nestjs/common';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { AuthCredentialsDto } from './dto/auth-credentials.dto';
import { User } from './user.entity';
import { hashPassword } from '../utils/common';
import { JwtService } from '@nestjs/jwt';
import { JwtPayload } from './jwt-payload.interface';

@Injectable()
export class AuthService {
constructor(
@Inject('USER_REPOSITORY')
private userRepository: Repository<User>,
private jwtService: JwtService,
) {}

async createUser(authCredentialsDto: AuthCredentialsDto): Promise<void> {
try {
const { username, password } = authCredentialsDto;
const salt = await bcrypt.genSalt();

const user = new User();
user.username = username;
user.salt = salt;
user.password = await hashPassword(password, salt);

await user.save();
} catch (err) {
if (err.code === 23505) {
//duplicate username
throw new ConflictException('Username already exists');
} else {
throw new InternalServerErrorException();
}
}
}

async validateUserPassword(
authCredentialsDto: AuthCredentialsDto,
): Promise<{ accessToken: string }> {
const { username, password } = authCredentialsDto;
const user = await this.userRepository.findOne({ username });
let existUsername = null;

if (user && (await user.validatePassword(password))) {
existUsername = user.username;
}

if (!existUsername) {
throw new UnauthorizedException('Invalid user credentials');
}

const payload: JwtPayload = { username };
const accessToken = await this.jwtService.sign(payload);

return { accessToken };
}
}
15 changes: 15 additions & 0 deletions src/auth/dto/auth-credentials.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsString, Matches, MaxLength, MinLength } from 'class-validator';
import { PASSWORD_PATTERN } from '../../utils/constants';

export class AuthCredentialsDto {
@IsString()
@MinLength(6)
@MaxLength(20)
username: string;

@IsString()
@MinLength(6)
@MaxLength(20)
@Matches(PASSWORD_PATTERN, { message: 'Password too weak' })
password: string;
}
9 changes: 9 additions & 0 deletions src/auth/get-user.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from './user.entity';

export const GetUser = createParamDecorator(
(data, ctx: ExecutionContext): User => {
const req = ctx.switchToHttp().getRequest();
return req.user;
},
);
3 changes: 3 additions & 0 deletions src/auth/jwt-payload.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface JwtPayload {
username: string;
}
30 changes: 30 additions & 0 deletions src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { Repository } from 'typeorm';
import { JwtPayload } from './jwt-payload.interface';
import { User } from './user.entity';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@Inject('USER_REPOSITORY')
private userRepository: Repository<User>,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'topSecret51',
});
}

async validate(payload: JwtPayload): Promise<User> {
const { username } = payload;
const user = await this.userRepository.findOne({ username });

if (!user) {
throw new UnauthorizedException('Invalid user token');
}

return user;
}
}
31 changes: 31 additions & 0 deletions src/auth/user.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
BaseEntity,
Column,
Entity,
PrimaryGeneratedColumn,
Unique,
} from 'typeorm';
import * as bcrypt from 'bcrypt';
import { hashPassword } from '../utils/common';

@Entity()
@Unique(['username'])
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;

@Column()
username: string;

@Column()
password: string;

@Column()
salt: string;

async validatePassword(password: string): Promise<boolean> {
const hash = await bcrypt.hash(password, this.salt);

return hash === this.password;
}
}
10 changes: 10 additions & 0 deletions src/auth/user.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Connection } from 'typeorm';
import { User } from './user.entity';

export const userProviders = [
{
provide: 'USER_REPOSITORY',
useFactory: (connection: Connection) => connection.getRepository(User),
inject: ['DATABASE_CONNECTION'],
},
];
5 changes: 4 additions & 1 deletion src/tasks/tasks.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
Patch,
Post,
Query,
UseGuards,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { filter } from 'rxjs';
import { CreateTaskDto } from './dto/create-task.dto';
import { GetTasksFilterDto } from './dto/get-tasks-filter.dto';
Expand All @@ -20,6 +22,7 @@ import { Task } from './task.entity';
import { TasksService } from './tasks.service';

@Controller('tasks')
@UseGuards(AuthGuard())
export class TasksController {
constructor(private tasksService: TasksService) {}

Expand All @@ -35,7 +38,7 @@ export class TasksController {
return this.tasksService.getTaskById(id);
}

// //Define parameter implicit
// Define parameter implicit
@Post()
@UsePipes(ValidationPipe)
createTask(@Body() createTaskDto: CreateTaskDto): Promise<Task> {
Expand Down
3 changes: 2 additions & 1 deletion src/tasks/tasks.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from '../auth/auth.module';
import { DatabaseModule } from '../database.module';
import { taskProviders } from './task.provider';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';

@Module({
// imports: [DatabaseModule, TypeOrmModule.forFeature([TaskRepository])],
imports: [DatabaseModule],
imports: [DatabaseModule, AuthModule],
controllers: [TasksController],
providers: [...taskProviders, TasksService],
})
Expand Down
8 changes: 8 additions & 0 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as bcrypt from 'bcrypt';

export const hashPassword = async (
password: string,
salt: string,
): Promise<string> => {
return bcrypt.hash(password, salt);
};
4 changes: 4 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const PASSWORD_PATTERN: RegExp =
/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{6,}$/;

export { PASSWORD_PATTERN };
Loading

0 comments on commit 812222b

Please sign in to comment.