기초적인 승인 시스템#
제출된 보고서 중 무의미한 데이터를 방지하기 위해 ‘관리자’와 관리자의 승인 개념을 도입한다. 우선은 관리자는 배제한체 승인에 관해서만 구현한다.
우선 report.entity 파일에 속성을 추가하자
1
2
3
| // report.entity.ts
@Column({ default: false })
approved: boolean;
|
이후 컨트롤러에 관련 메서드를 추가한다.
1
2
3
| // reports.controller.ts
@Patch('/:id')
approveReport(@Param('id') id: string, @Body() body: ApprovedReportDto) {}
|
reports의 dtos 디렉토리에 새로운 dto를 추가하자. report dto 에도 approved 속성을 추가해야한다.
1
2
3
4
5
6
7
8
9
10
11
| // approve-report.dto.ts
import { IsBoolean } from 'class-validator';
export class ApproveReportDto {
@IsBoolean()
approved: boolean;
}
// report.dto.ts
@Expose()
approved: boolean;
|
다시 컨트롤러로 돌아가서 마무리한다.
1
2
3
4
5
| // reports.controller.ts
@Patch('/:id')
approveReport(@Param('id') id: string, @Body() body: ApproveReportDto) {
return this.reportsService.changeApproval(id, body.approved);
}
|
서비스에 changeApproval도 추가해보자
1
2
3
4
5
6
7
8
9
10
| // reports.service.ts
async changeApproval(id: string, approved: boolean) {
const report = await this.repo.findOne({ where: { id: parseInt(id) } });
if (!report) {
throw new NotFoundException('report not found');
}
report.approved = approved;
return this.repo.save(report);
}
|
이제 API 테스트를 진행해본다. approved 가 false로 추가되어있으면 성공이다. id를 기억해두자
새로운 테스트도 추가하자. report 뒤에 id를 제공한다.
1
2
3
| requests.http
PATCH http://localhost:3000/reports/3
content
|
approved가 true로 바뀌었는지 확인해보자.
Authentication VS Authorization#
Authentication : 누가 요청했는가 파악하기
Authorization : 요청한 사람이 권한이 있는지 확인하기
기존 users 디렉토리 내 인터셉터는 쿠키를 통해 요청한 사람이 누구인지 확인하였다.
guards는 로그인 한사람이 누구인지 파악하는 기능이 있다.
따라서 우리는 AdminGuard를 만들어서 request.currentUser가 관리자인지 확인하도록 middleware를 추가할 계획이다.
권한 가드 추가하기#
user entity에 관리자 속성을 추가하자
1
2
3
| // user.entity.ts
@Column({ default: true })
admin: boolean;
|
테스트를 위해 true로 설정해놓았다.
이제 src 폴더 내 guards 폴더에 admin.guard.ts 를 추가하자
1
2
3
4
5
6
7
8
9
10
11
12
13
| // admin.guard.ts
import { CanActivate, ExecutionContext } from '@nestjs/common';
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
if (!request.currentUser) {
return false;
}
return request.currentUser.admin;
}
}
|
컨트롤러에 연결한다.
1
2
3
4
5
6
7
8
| // reports.controller.ts
import { AdminGuard } from '../guards/admin.guard';
@Patch('/:id')
@UseGuards(AdminGuard)
approveReport(@Param('id') id: string, @Body() body: ApproveReportDto) {
return this.reportsService.changeApproval(id, body.approved);
}
|
서버를 켜서 로그인하고 report를 보낸 후 승인하는 요청을 보내면 403 Forbidden 에러가 발생한다.
Guard가 제대로 작동하지 않는 이유#
현재 요청의 Flow를 살펴보자
- 요청
- 쿠키-세션 미들웨어
- AdminGuard
- Request Handler
- Response
3-4, 4-5 사이에 CurrentUser Interceptor가 개입한다.
인터셉터를 middleware로 변경하여 AdminGuard 이전으로 순서를 변경해야 한다.
인터셉터는 미들웨어와 guard 이후에 실행된다. 따라서 미들웨어와 guard는 인터셉터가 생성하는 유저 정보나 인스턴스를 참조할 수 없다.
Interceptor 미들웨어로 바꾸기#
user 폴더에 middlewares 폴더를 만들고 내부에 current-user.middleware.ts를 만들자. 주석처리부분은 일단 냅둬보
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // current-user,middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { UsersService } from ' ../users.service';
@Injectable()
export class CurrentUserMiddleware implements NestMiddleware {
constructor(private usersService: UsersService) {}
async use(req: Request, res: Response, next: NextFunction) {
const { userId } = req.session || {};
if (userId) {
const user = await this.usersService.findOne(userId);
// @ts-ignore
req.currentUser = user;
}
next();
}
}
|
user 모듈을 수정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // users.module.ts
import { Module, MiddlewareConsumer } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { AuthService } from './auth.service';
import { User } from './user.entity';
import { CurrentUserMiddleware } from './middlewares/current-user.middleware';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService, AuthService],
})
export class UsersModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(CurrentUserMiddleware).forRoutes('*');
}
}
|
API Client 테스트를 해보자. 정상적으로 보내진다.
Type 정의 에러 수정하기#
새로 만든 미들웨어 파일에서 주석처리한 부분을 지우면 currentUser에 오류가 발생한다. currentUser 속성이 없기 떄문이다. 이를 해결하기 위해 요청 인터페이스를 수정한다.
1
2
3
4
5
6
7
8
9
10
| // current-user.middleware.ts
import { User } from'../user.entity';
declare global {
namespace Express {
interface Request {
currentUser?: User;
}
}
}
|
이렇게 하면 주석을 지워도 더이상 오류가 발생하지 않는다.
쿼리 문자열이 가져오는 정보의 유효성 검사하기#
report의 dtos 디렉토리에 get-estimate.dot.ts를 만들자. create-report dto파일을 복사 붙여넣어서 불필요한 속성은 지워준다. price만 지워버린다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // get-estimate.dto.ts
import {
IsString,
IsNumber,
Min,
Max,
IsLongitude,
IsLatitude,
} from 'class-validator';
export class GetEstimateDto { // 이름 잘 바꿔주자
@IsString()
make: string;
@IsString()
model: string;
@IsNumber()
@Min(1930)
@Max(2050)
year: number;
@IsNumber()
@Min(0)
@Max(1000000)
mileage: number;
@IsLongitude()
lng: number;
@IsLatitude()
lat: number;
}
|
컨트롤러에 추가하고 API 테스트 코드도 추가한다.
1
2
3
| // reports.controller.ts
@Get()
getEstimate(@Query() query: GetEstimateDto) {}
|
1
2
3
4
| // requests.http
### Get an estimate for an existing vehicle
GET http://localhost:3000/reports?make=toyota&model=corolla&lng=0&lat=0&mileage=10000&year=1980
|
요청윈 위에 테스트 코드에 있는 속성을 넣는다. 서버를 키고 요청을 보내보면 오류가 발생한다.
우리가 숫자를 보내고 있지만 서버에선 이 숫자들을 문자열로 받아들이고 있기 떄문이다.
쿼리 문자열 데이터 변환하기#
dto 파일을 수정하자
1
2
3
4
5
6
| // get-estimate.dto.ts
@Transform(({ value }) => parseInt(value))
@IsNumber()
@Min(1930)
@Max(2050)
year: number;
|
위처럼 문자열을 숫자로 받기 위한 처리가 필요하다. 모두 수정해주자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| // get-estimate.dto.ts
import {
IsString,
IsNumber,
Min,
Max,
IsLongitude,
IsLatitude,
} from 'class-validator';
import { Transform } from 'class-transformer';
export class GetEstimateDto {
@IsString()
make: string;
@IsString()
model: string;
@Transform(({ value }) => parseInt(value))
@IsNumber()
@Min(1930)
@Max(2050)
year: number;
@Transform(({ value }) => parseInt(value))
@IsNumber()
@Min(0)
@Max(1000000)
mileage: number;
@Transform(({ value }) => parseFloat(value))
@IsLongitude()
lng: number;
@Transform(({ value }) => parseFloat(value))
@IsLatitude()
lat: number;
}
|
올바르게 받는지 확인하기 위해 컨트롤러의 getEstimate 메서드에 console.log(query);를 입력하고
API 테스트를 돌려 콘솔창을 확인한다. 숫자가 알맞게 들어온 것을 확인할 수 있다.
추정 가격 생성#
기준을 세워 데이터에서 비슷한걸 찾자.
- 같은 제조사, 모델
- 경도 위도 차이가 5도 미만
- 연식 차이가 3년 이내
- 주행거리가 가장 가까운 레코드
기준의 근접한 보고서 3개를 찾아 해당 가격의 평균값을 반