Skip to content

Commit

Permalink
๐Ÿ”€ Merge pull request #136 from qkrwogk/feat/board-security
Browse files Browse the repository at this point in the history
[BE] Board๋ชจ๋“ˆ ๋ณด์•ˆ : ๊ฒŒ์‹œ๊ธ€ ๋ณธ๋ฌธ ์•”๋ณตํ˜ธํ™”,Entity ๊ด€๊ณ„์ ์šฉ, AuthGuard ์ ์šฉ, ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ ์‹œ author ์„œ๋ฒ„๊ฐ€ ์‚ฝ์ž…
  • Loading branch information
SongJSeop authored Nov 22, 2023
2 parents a44b101 + 628ff4c commit 5ae8739
Show file tree
Hide file tree
Showing 14 changed files with 116 additions and 16 deletions.
10 changes: 10 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file modified .yarn/install-state.gz
Binary file not shown.
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"crypto": "^1.0.1",
"dotenv": "^16.3.1",
"ioredis": "^5.3.2",
"mysql2": "^3.6.3",
Expand Down
4 changes: 3 additions & 1 deletion packages/server/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConfig } from '../config/jwt.config';
import { RedisRepository } from './redis.repository';
import { CookieAuthGuard } from './cookie-auth.guard';

@Module({
imports: [
Expand All @@ -15,6 +16,7 @@ import { RedisRepository } from './redis.repository';
TypeOrmModule.forFeature([User]),
],
controllers: [AuthController],
providers: [AuthService, RedisRepository],
providers: [AuthService, CookieAuthGuard, RedisRepository],
exports: [JwtModule, CookieAuthGuard, RedisRepository],
})
export class AuthModule {}
5 changes: 5 additions & 0 deletions packages/server/src/auth/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Board } from 'src/board/entities/board.entity';
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';

Expand All @@ -21,4 +23,7 @@ export class User {

@CreateDateColumn()
created_at: Date;

@OneToMany(() => Board, (board) => board.user)
boards: Board[];
}
22 changes: 21 additions & 1 deletion packages/server/src/board/board.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
UsePipes,
ValidationPipe,
ParseIntPipe,
UseGuards,
Req,
} from '@nestjs/common';
import { BoardService } from './board.service';
import { CreateBoardDto } from './dto/create-board.dto';
Expand All @@ -28,25 +30,33 @@ import {
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { CreateImageDto } from './dto/create-image.dto';
import { CookieAuthGuard } from 'src/auth/cookie-auth.guard';

@Controller('board')
@ApiTags('๊ฒŒ์‹œ๊ธ€ API')
export class BoardController {
constructor(private readonly boardService: BoardService) {}

@Post()
@UseGuards(CookieAuthGuard)
@UsePipes(ValidationPipe)
@ApiOperation({ summary: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ', description: '๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.' })
@ApiCreatedResponse({ status: 201, description: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ์„ฑ๊ณต' })
@ApiBadRequestResponse({
status: 400,
description: '์ž˜๋ชป๋œ ์š”์ฒญ์œผ๋กœ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ์‹คํŒจ',
})
createBoard(@Body() createBoardDto: CreateBoardDto): Promise<Board> {
createBoard(
@Req() req,
@Body() createBoardDto: CreateBoardDto,
): Promise<Board> {
if (req.user && req.user.nickname)
createBoardDto.author = req.user.nickname;
return this.boardService.createBoard(createBoardDto);
}

@Get()
@UseGuards(CookieAuthGuard)
@ApiOperation({ summary: '๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ', description: '๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.' })
@ApiOkResponse({ status: 200, description: '๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ ์„ฑ๊ณต' })
@ApiBadRequestResponse({
Expand All @@ -58,6 +68,7 @@ export class BoardController {
}

@Get('by-author')
@UseGuards(CookieAuthGuard)
@ApiOperation({
summary: '์ž‘์„ฑ์ž๋ณ„ ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ',
description: '์ž‘์„ฑ์ž๋ณ„ ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.',
Expand All @@ -72,6 +83,7 @@ export class BoardController {
}

@Get(':id')
@UseGuards(CookieAuthGuard)
@ApiOperation({
summary: '๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ์กฐํšŒ',
description: '๊ฒŒ์‹œ๊ธ€์„ ์ƒ์„ธ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.',
Expand All @@ -86,6 +98,7 @@ export class BoardController {
}

@Patch(':id')
@UseGuards(CookieAuthGuard)
@UsePipes(ValidationPipe)
@ApiOperation({ summary: '๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •', description: '๊ฒŒ์‹œ๊ธ€์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.' })
@ApiOkResponse({ status: 200, description: '๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • ์„ฑ๊ณต' })
Expand All @@ -94,13 +107,17 @@ export class BoardController {
description: '์ž˜๋ชป๋œ ์š”์ฒญ์œผ๋กœ ๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • ์‹คํŒจ',
})
updateBoard(
@Req() req,
@Param('id', ParseIntPipe) id: number,
@Body() updateBoardDto: UpdateBoardDto,
) {
if (req.user && req.user.nickname)
updateBoardDto.author = req.user.nickname;
return this.boardService.updateBoard(id, updateBoardDto);
}

@Patch(':id/like')
@UseGuards(CookieAuthGuard)
@ApiOperation({
summary: '๊ฒŒ์‹œ๊ธ€ ์ข‹์•„์š”',
description: '๊ฒŒ์‹œ๊ธ€์— ์ข‹์•„์š”๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.',
Expand All @@ -115,6 +132,7 @@ export class BoardController {
}

@Patch(':id/unlike')
@UseGuards(CookieAuthGuard)
@ApiOperation({
summary: '๊ฒŒ์‹œ๊ธ€ ์ข‹์•„์š” ์ทจ์†Œ',
description: '๊ฒŒ์‹œ๊ธ€์— ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค.',
Expand All @@ -129,6 +147,7 @@ export class BoardController {
}

@Delete(':id')
@UseGuards(CookieAuthGuard)
@ApiOperation({ summary: '๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ', description: '๊ฒŒ์‹œ๊ธ€์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.' })
@ApiOkResponse({ status: 200, description: '๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ ์„ฑ๊ณต' })
@ApiNotFoundResponse({
Expand All @@ -140,6 +159,7 @@ export class BoardController {
}

@Post(':id/image')
@UseGuards(CookieAuthGuard)
@UseInterceptors(FileInterceptor('file', { dest: './uploads' }))
@UsePipes(ValidationPipe)
@ApiOperation({
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/board/board.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { BoardController } from './board.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Board } from './entities/board.entity';
import { Image } from './entities/image.entity';
import { AuthModule } from 'src/auth/auth.module';

@Module({
imports: [TypeOrmModule.forFeature([Board, Image])],
imports: [TypeOrmModule.forFeature([Board, Image]), AuthModule],
controllers: [BoardController],
providers: [BoardService],
})
Expand Down
15 changes: 12 additions & 3 deletions packages/server/src/board/board.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Repository } from 'typeorm';
import { unlinkSync } from 'fs';
import { CreateImageDto } from './dto/create-image.dto';
import { Image } from './entities/image.entity';
import { encryptAes, decryptAes } from 'src/utils/aes.util';

@Injectable()
export class BoardService {
Expand All @@ -27,7 +28,7 @@ export class BoardService {

const board = this.boardRepository.create({
title,
content,
content: encryptAes(content), // AES ์•”ํ˜ธํ™”ํ•˜์—ฌ ์ €์žฅ
author,
});
const created: Board = await this.boardRepository.save(board);
Expand All @@ -50,12 +51,20 @@ export class BoardService {
if (!found) {
throw new NotFoundException(`Not found board with id: ${id}`);
}
if (found.content) {
found.content = decryptAes(found.content); // AES ๋ณตํ˜ธํ™”ํ•˜์—ฌ ๋ฐ˜ํ™˜
}
return found;
}

async updateBoard(id: number, updateBoardDto: UpdateBoardDto) {
const board: Board = await this.findBoardById(id);

// updateBoardDto.content๊ฐ€ ์กด์žฌํ•˜๋ฉด AES ์•”ํ˜ธํ™”ํ•˜์—ฌ ์ €์žฅ
if (updateBoardDto.content) {
updateBoardDto.content = encryptAes(updateBoardDto.content);
}

const updatedBoard: Board = await this.boardRepository.save({
...board,
...updateBoardDto,
Expand Down Expand Up @@ -102,7 +111,7 @@ export class BoardService {
}

// ์ด๋ฏธ ํŒŒ์ผ์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
if (board.image_id) {
if (board.image) {
unlinkSync(file.path); // ํŒŒ์ผ ์‚ญ์ œ
}

Expand All @@ -115,7 +124,7 @@ export class BoardService {
});
const updatedImage = await this.imageRepository.save(image);

board.image_id = updatedImage.id;
board.image = updatedImage.id;
const updatedBoard = await this.boardRepository.save(board);

return updatedBoard;
Expand Down
17 changes: 9 additions & 8 deletions packages/server/src/board/dto/create-board.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ export class CreateBoardDto {
})
content: string;

@IsNotEmpty({ message: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ์ž…๋‹ˆ๋‹ค.' })
@IsString({ message: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž๋Š” ๋ฌธ์ž์—ด๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.' })
@MaxLength(50, { message: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž๋Š” 50์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.' })
@ApiProperty({
description: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž',
example: 'test author',
required: true,
})
// ์„œ๋ฒ„์—์„œ ์ง์ ‘ ์‚ฝ์ž…ํ•ด์ฃผ๋„๋ก ๋ณ€๊ฒฝ (validation ์ œ๊ฑฐ)
// @IsNotEmpty({ message: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ์ž…๋‹ˆ๋‹ค.' })
// @IsString({ message: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž๋Š” ๋ฌธ์ž์—ด๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.' })
// @MaxLength(50, { message: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž๋Š” 50์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.' })
// @ApiProperty({
// description: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž',
// example: 'test author',
// required: true,
// })
author: string;
}
13 changes: 11 additions & 2 deletions packages/server/src/board/entities/board.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Image } from './image.entity';
import { User } from 'src/auth/entities/user.entity';

@Entity()
export class Board extends BaseEntity {
Expand All @@ -30,6 +35,10 @@ export class Board extends BaseEntity {
@Column({ type: 'int', default: 0 })
like_cnt: number;

@Column({ type: 'int', nullable: true })
image_id: number;
@OneToOne(() => Image, { nullable: true })
@JoinColumn()
image: number;

@ManyToOne(() => User, (user) => user.boards, { onDelete: 'CASCADE' })
user: User;
}
8 changes: 8 additions & 0 deletions packages/server/src/config/aes.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { configDotenv } from 'dotenv';
configDotenv();

export const aesConfig = {
password: process.env.AES_PASSWORD,
salt: process.env.AES_SALT,
iv: Buffer.alloc(16, 0),
};
26 changes: 26 additions & 0 deletions packages/server/src/utils/aes.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as crypto from 'crypto';
import { aesConfig } from '../config/aes.config';

const encryptAes = (plainText) => {
const algorithm = 'aes-256-cbc'; // ์•”ํ˜ธ ์•Œ๊ณ ๋ฆฌ์ฆ˜
const key = crypto.scryptSync(aesConfig.password, aesConfig.salt, 32); // ์•”ํ˜ธํ™” ํ‚ค
const iv = aesConfig.iv; // ์ดˆ๊ธฐํ™” ๋ฒกํ„ฐ

const cipher = crypto.createCipheriv(algorithm, key, iv);
let cipherText = cipher.update(plainText, 'utf8', 'base64');
cipherText += cipher.final('base64');
return cipherText;
};

const decryptAes = (cipherText) => {
const algorithm = 'aes-256-cbc'; // ์•”ํ˜ธ ์•Œ๊ณ ๋ฆฌ์ฆ˜
const key = crypto.scryptSync(aesConfig.password, aesConfig.salt, 32); // ์•”ํ˜ธํ™” ํ‚ค
const iv = aesConfig.iv; // ์ดˆ๊ธฐํ™” ๋ฒกํ„ฐ

const decipher = crypto.createDecipheriv(algorithm, key, iv);
let plainText = decipher.update(cipherText, 'base64', 'utf8');
plainText += decipher.final('utf8');
return plainText;
};

export { encryptAes, decryptAes };
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3456,6 +3456,13 @@ __metadata:
languageName: node
linkType: hard

"crypto@npm:^1.0.1":
version: 1.0.1
resolution: "crypto@npm:1.0.1"
checksum: fcf7dbd68ac5415b7fde7d7208fe203038e92e83e8a8fcf6e86ab4771ce3dd026d6967a990ba56b9d1c771378210814d5c90d907d3739fbd1723d552ad6c8ab8
languageName: node
linkType: hard

"csstype@npm:^3.0.2":
version: 3.1.2
resolution: "csstype@npm:3.1.2"
Expand Down Expand Up @@ -7703,6 +7710,7 @@ __metadata:
class-transformer: "npm:^0.5.1"
class-validator: "npm:^0.14.0"
cookie-parser: "npm:^1.4.6"
crypto: "npm:^1.0.1"
dotenv: "npm:^16.3.1"
eslint: "npm:^8.42.0"
eslint-config-prettier: "npm:^9.0.0"
Expand Down

0 comments on commit 5ae8739

Please sign in to comment.