Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: create, get and update board #50

Merged
merged 9 commits into from
Nov 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"typeorm": "^0.2.38"
"typeorm": "^0.2.38",
"uuid": "^8.3.2"
},
"devDependencies": {
"@nestjs/cli": "^8.0.0",
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { BoardsModule } from './boards/boards.module';
import * as Joi from 'joi';

@Module({
Expand All @@ -24,6 +25,7 @@ import * as Joi from 'joi';
DatabaseModule,
UsersModule,
AuthModule,
BoardsModule,
],
controllers: [],
providers: [],
Expand Down
15 changes: 6 additions & 9 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import {
Res,
} from '@nestjs/common';
import { Response } from 'express';
import JwtAuthenticationGuard from './guards/jwtAuth.guard';
import JwtAuthenticationGuard from '../guards/jwtAuth.guard';
import { AuthService } from './auth.service';
import RegisterDto from '../users/dto/register.dto';
import { LocalAuthGuard } from './guards/localAuth.guard';
import { LocalAuthGuard } from '../guards/localAuth.guard';
import RequestWithUser from '../interfaces/requestWithUser.interface';
import { UsersService } from '../users/users.service';
import JwtRefreshGuard from './guards/jwtRefreshAuth.guard';
import { LoginUserDto } from 'src/users/dto/login.dto';
import JwtRefreshGuard from '../guards/jwtRefreshAuth.guard';
import { LoginUserDto } from '../users/dto/login.dto';

@Controller('auth')
export class AuthController {
Expand All @@ -26,7 +26,7 @@ export class AuthController {
) {}

@Post('register')
async register(@Body() registrationData: RegisterDto) {
register(@Body() registrationData: RegisterDto) {
return this.authService.register(registrationData);
}

Expand Down Expand Up @@ -62,9 +62,6 @@ export class AuthController {
@UseGuards(JwtRefreshGuard)
@Get('refresh')
refresh(@Req() request: RequestWithUser) {
const accessToken = this.authService.getJwtAccessToken(
request.user._id.toString(),
);
return { accessToken };
return this.authService.getJwtAccessToken(request.user._id.toString());
}
}
11 changes: 8 additions & 3 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from 'src/users/users.module';
import { UsersModule } from '../users/users.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { LocalStrategy } from './strategy/local.strategy';
import { JwtStrategy } from './strategy/jwt.strategy';
import { JwtRefreshTokenStrategy } from './strategy/refresh.strategy';
import {
describe,
JWT_ACCESS_TOKEN_SECRET,
JWT_ACCESS_TOKEN_EXPIRATION_TIME,
} from '../constants/jwt';

@Module({
imports: [
Expand All @@ -18,10 +23,10 @@ import { JwtRefreshTokenStrategy } from './strategy/refresh.strategy';
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_ACCESS_TOKEN_SECRET'),
secret: configService.get(describe(JWT_ACCESS_TOKEN_SECRET)),
signOptions: {
expiresIn: `${configService.get(
'JWT_ACCESS_TOKEN_EXPIRATION_TIME',
describe(JWT_ACCESS_TOKEN_EXPIRATION_TIME),
)}s`,
},
}),
Expand Down
36 changes: 27 additions & 9 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import { compare, encrypt } from '../utils/bcrypt';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import TokenPayload from '../interfaces/tokenPayload.interface';
import {
JWT_ACCESS_TOKEN_EXPIRATION_TIME,
JWT_ACCESS_TOKEN_SECRET,
JWT_REFRESH_TOKEN_EXPIRATION_TIME,
JWT_REFRESH_TOKEN_SECRET,
describe,
} from '../constants/jwt';
import { INVALID_CREDENTIALS, EMAIL_EXISTS } from '../constants/httpExceptions';

@Injectable()
export class AuthService {
Expand All @@ -22,7 +30,10 @@ export class AuthService {
user.password = undefined;
return user;
} catch (error) {
throw new HttpException('INVALID_CREDENTIALS', HttpStatus.BAD_REQUEST);
throw new HttpException(
describe(INVALID_CREDENTIALS),
HttpStatus.BAD_REQUEST,
);
}
}

Expand All @@ -32,7 +43,10 @@ export class AuthService {
) {
const isPasswordMatching = await compare(plainTextPassword, hashedPassword);
if (!isPasswordMatching) {
throw new HttpException('INVALID_CREDENTIALS', HttpStatus.BAD_REQUEST);
throw new HttpException(
describe(INVALID_CREDENTIALS),
HttpStatus.BAD_REQUEST,
);
}
}

Expand All @@ -47,7 +61,7 @@ export class AuthService {
return createdUser;
} catch (error) {
if (error?.code === errors.UniqueViolation) {
throw new HttpException('EMAIL_EXISTS', HttpStatus.BAD_REQUEST);
throw new HttpException(describe(EMAIL_EXISTS), HttpStatus.BAD_REQUEST);
}
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
Expand All @@ -56,13 +70,15 @@ export class AuthService {
public getJwtAccessToken(userId: string) {
const payload: TokenPayload = { userId };
const token = this.jwtService.sign(payload, {
secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET'),
secret: this.configService.get(describe(JWT_ACCESS_TOKEN_SECRET)),
expiresIn: `${this.configService.get(
'JWT_ACCESS_TOKEN_EXPIRATION_TIME',
describe(JWT_ACCESS_TOKEN_EXPIRATION_TIME),
)}s`,
});
return {
expiresIn: this.configService.get('JWT_ACCESS_TOKEN_EXPIRATION_TIME'),
expiresIn: this.configService.get(
describe(JWT_ACCESS_TOKEN_EXPIRATION_TIME),
),
token,
};
}
Expand All @@ -71,13 +87,15 @@ export class AuthService {
const payload: TokenPayload = { userId };

const token = this.jwtService.sign(payload, {
secret: this.configService.get('JWT_REFRESH_TOKEN_SECRET'),
secret: this.configService.get(describe(JWT_REFRESH_TOKEN_SECRET)),
expiresIn: `${this.configService.get(
'JWT_REFRESH_TOKEN_EXPIRATION_TIME',
describe(JWT_REFRESH_TOKEN_EXPIRATION_TIME),
)}d`,
});
return {
expiresIn: this.configService.get('JWT_REFRESH_TOKEN_EXPIRATION_TIME'),
expiresIn: this.configService.get(
describe(JWT_REFRESH_TOKEN_EXPIRATION_TIME),
),
token,
};
}
Expand Down
2 changes: 1 addition & 1 deletion backend/src/auth/strategy/local.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { AuthService } from '../auth.service';
import UserEntity from 'src/users/entity/user.entity';
import UserEntity from '../../users/entity/user.entity';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
Expand Down
60 changes: 60 additions & 0 deletions backend/src/boards/boards.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
Body,
Controller,
Get,
Patch,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { BoardsService } from './boards.service';
import BoardDto from './dto/board.dto';
import JwtAuthenticationGuard from '../guards/jwtAuth.guard';
import { UsersService } from '../users/users.service';
import RequestWithUser from '../interfaces/requestWithUser.interface';
import BoardPasswordDto from './dto/boardPassword.dto';
import UpdateLockedDto from './dto/updateLockedDto';

@Controller('boards')
export class BoardsController {
constructor(
private readonly boardService: BoardsService,
private readonly usersService: UsersService,
) {}

@UseGuards(JwtAuthenticationGuard)
@Post()
async createBoard(@Body() boardData: BoardDto) {
const user = await this.usersService.getByEmail(boardData.createdBy.email);
if (user) {
return this.boardService.create(boardData);
}
}

@UseGuards(JwtAuthenticationGuard)
@Get()
boards(@Req() request: RequestWithUser) {
return this.boardService.getAllBoards(request.user.email);
}

@Post(':id')
getBoard(@Req() request, @Body() passwordDto?: BoardPasswordDto) {
return this.boardService.getBoard(request.params.id, passwordDto.password);
}

@UseGuards(JwtAuthenticationGuard)
@Patch(':id/updateLocked')
updateLocked(
@Req() request: RequestWithUser,
@Body() updateLockedDto: UpdateLockedDto,
) {
const boardId = request.params.id;
const user = request.user;
return this.boardService.updateLocked(
updateLockedDto.locked,
updateLockedDto.password,
boardId,
user.email,
);
}
}
13 changes: 13 additions & 0 deletions backend/src/boards/boards.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardsService } from './boards.service';
import { BoardsController } from './boards.controller';
import BoardEntity from './entity/board.entity';
import { UsersModule } from '../users/users.module';

@Module({
imports: [TypeOrmModule.forFeature([BoardEntity]), UsersModule],
providers: [BoardsService],
controllers: [BoardsController],
})
export class BoardsModule {}
77 changes: 77 additions & 0 deletions backend/src/boards/boards.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import BoardEntity from './entity/board.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import BoardDto from './dto/board.dto';
import { compare, encrypt } from '../utils/bcrypt';
import {
BOARD_NOT_FOUND,
BOARDS_NOT_FOUND,
describe,
} from '../constants/httpExceptions';

@Injectable()
export class BoardsService {
constructor(
@InjectRepository(BoardEntity)
private boardsRepository: Repository<BoardEntity>,
) {}

async create(boardData: BoardDto) {
if (boardData.password) {
boardData.password = await encrypt(boardData.password);
}
const newBoard = this.boardsRepository.create(boardData);
return this.boardsRepository.save(newBoard);
}

async getAllBoards(email: string) {
const boards = await this.boardsRepository.find({
where: {
'createdBy.email': email,
},
});
if (boards) return boards;
throw new HttpException(describe(BOARDS_NOT_FOUND), HttpStatus.NOT_FOUND);
}

async getBoardFromRepo(boardId: string) {
const board = await this.boardsRepository.findOne(boardId);
if (board) return board;
throw new HttpException(describe(BOARD_NOT_FOUND), HttpStatus.NOT_FOUND);
}

async getBoard(id: string, password?: string) {
const board = await this.getBoardFromRepo(id);
if (!board.locked) return board;

const isPasswordMatched = await compare(password, board.password);
if (isPasswordMatched) return board;

return { locked: true };
}

async updateLocked(
locked: boolean,
password: string | null,
boardId: string,
email: string,
) {
const board = await this.getBoardFromRepo(boardId);
if (board.createdBy.email !== email)
throw new HttpException(
describe(BOARD_NOT_FOUND),
HttpStatus.UNAUTHORIZED,
);

if (locked === false) {
password = null;
}
const hashedPw = password ? await encrypt(password) : null;
await this.boardsRepository.update(boardId, {
locked,
password: hashedPw,
});
return this.getBoardFromRepo(boardId);
}
}
36 changes: 36 additions & 0 deletions backend/src/boards/dto/board.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
IsString,
IsNotEmpty,
ArrayNotEmpty,
ArrayMinSize,
ValidateNested,
IsEmail,
IsOptional,
} from 'class-validator';
import { Transform, TransformFnParams, Type } from 'class-transformer';
import ColumnDto from './column.dto';
import { UserDto } from '../../users/dto/user.dto';

class BoardDto {
@IsString()
@IsNotEmpty()
@Transform(({ value }: TransformFnParams) => value.trim())
title: string;

@ArrayNotEmpty()
@ArrayMinSize(3)
@IsNotEmpty()
@ValidateNested({ each: true })
@Type((type) => ColumnDto)
columns: ColumnDto[];

@IsNotEmpty()
locked: boolean;

@IsNotEmpty()
createdBy: UserDto;

@IsOptional()
password: string;
}
export default BoardDto;
8 changes: 8 additions & 0 deletions backend/src/boards/dto/boardPassword.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IsOptional } from 'class-validator';

class BoardPasswordDto {
@IsOptional()
password: string;
}

export default BoardPasswordDto;
Loading