본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
역할 가드(Guard)는 roles.decorator를 기반으로 인가를 할지 결정함
즉, roles.decorator에서 알려주는 역할이 통과될 수 있는지 검사함
RolesGuard는 AuthGuard('jwt')를 상속하기 때문에 req.user를 통해서 로그인한 사용자의 정보에 접근할 수 있음
// roles.guard.ts
@Injectable()
export class RolesGuard extends AuthGuard('jwt') implements CanActivate {
constructor(private readonly reflector: Reflector) {
super();
}
// 로그인한 사용자가 해당 역할에 맞는지 확인
async canActivate(context: ExecutionContext): Promise<boolean> {
// jwt 검증이 통과 되었는지 확인
const authenticated = await super.canActivate(context);
if (!authenticated) return false;
// @Roles(Role.Admin) -> 'roles'에 [Role.Admin] 배열이 담겨 있음
// 즉, requiredRoles에 [Role.Admin] 배열이 들어감
// reflector를 통해서 메타데이터를 탐색 후 'roles' 키의 값을 가져옴
const requireRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
context.getHandler(),
context.getClass(),
]);
const { user } = context.switchToHttp().getRequest();
// 사용자의 역할이 requireRoles 배열에 해닿하는지 확인
if (!requireRoles.some((role) => user.role === role)) {
throw new ForbiddenException('접근할 권한이 없습니다.');
}
return true;
}
}
그리고 권한이 주어진 역할을 설정하기 위한 커스텀 데코레이터를 사용
매개변수 roles를 받아서 'roles'라는 이름의 메타데이터로 저장
여기서 메타데이터는 빌드 타임에 선언해 둔 메타데이터를 활용하여 런타임에 동작을 제어할 수 있는 강력한 방법이라고 할 수 있음
코드 자체는 매우 간단하지만 아래와 같은 코드에서 메타데이터를 설정하면 RolesGuard에서 아주 간단하게 사용자가 설정한 역할 데이터를 가져올 수 있음
// 역할(Role)을 위한 커스텀 데코레이터
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
이번 프로젝트에서는 이미지 업로드 API를 따로 구현함
기존에는 Multipart를 이용해서 다른 코드들과 같이 값을 넣었으나 Insomnia나 Swagger에서 양식의 차이로 코드 작성이 어렵기 때문에 이러한 이미지 데이터를 다른 API로 따로 받기로 함
우선 필요한 패키지를 설치해야 함
# Node.js 애플리케이션에서 S3 서비스를 사용하기 위한 패키지
npm install @aws-sdk/client-s3
FileInterceptor
를 설정@ApiTags('이미지')
@Controller('images')
export class ImagesController {
constructor(private readonly imagesService: ImagesService) {}
// 이미지 업로드 API
@ApiBearerAuth()
@UseGuards(RolesGuard)
@Roles(Role.ADMIN)
@UseInterceptors(FilesInterceptor('image'))
@ApiImages('image')
@Post()
async uploadImage(@UploadedFiles() files: Express.Multer.File[]) {
return await this.imagesService.uploadImage(files, 5);
}
}
서비스에서는 S3와 연결하기 위한 기본 세팅과 업로드에 필요한 데이터를 재구성해서 S3에 업로드하는 코드로 구성되어 있음
그리고 혹시 이미지를 업로드하고 게시물을 등록하는 과정에서 트랜젝션 에러가 발생하는 경우 S3의 이미지 파일도 삭제하기 위해서 따로 S3 롤백 함수를 구현함
@Injectable()
export class ImagesService {
s3: S3;
constructor(private readonly configService: ConfigService) {
this.s3 = new S3({
region: this.configService.get('AWS_S3_REGION'),
credentials: {
accessKeyId: this.configService.get('AWS_S3_ACCESS_KEY'),
secretAccessKey: this.configService.get('AWS_S3_SECRET_KEY'),
},
});
}
// 이미지를 S3에 업로드
async imageUploadToS3(
fileName: string, // 업로드될 파일의 이름
file: Express.Multer.File, // 업로드할 파일
ext: string // 파일 확장자
) {
try {
// AWS S3에 이미지 업로드 명령을 생성
// 파일 이름, 파일 버퍼, 파일 접근 권한, 파일 타입 등을 설정
const command = new PutObjectCommand({
Bucket: this.configService.get('AWS_BUCKET'), // S3 버킷 이름
Key: fileName, // 업로드될 파일의 이름
Body: file.buffer, // 업로드할 파일
ACL: 'public-read', // 파일 접근 권한
ContentType: `image/${ext}`, // 파일 타입
});
// 생성된 명령을 S3에 전달하여 이미지 업로드
await this.s3.send(command);
// 업로드된 이미지의 URL을 반환
return `https://s3.${process.env.AWS_S3_REGION}.amazonaws.com/${process.env.AWS_BUCKET}/${fileName}`;
} catch (err) {
console.error(err);
throw new InternalServerErrorException(
'파일 업로드가 실패했습니다. 관리자에게 문의해 주세요.'
);
}
}
// 사용자가 입력한 이미지 데이터를 받아서 S3에 전달 (이미지 업로드)
async uploadImage(files: Express.Multer.File[], maxFilesLength: number) {
if (files.length === 0) {
throw new BadRequestException('이미지를 입력해 주세요.');
}
if (files.length > maxFilesLength) {
throw new BadRequestException(`${maxFilesLength}장 이하로 업로드 가능합니다.`);
}
const allowedExtensions = ['.png', '.jpg', '.jpeg', '.bmp', '.gif'];
// 오늘 날짜 구하기
const today = new Date();
const currentYear = today.getFullYear();
const currentMonth = today.getMonth() + 1;
const currentDate = today.getDate();
const date = `${currentYear}-${currentMonth}-${currentDate}`;
const imageUrls: object[] = [];
await Promise.all(
files.map(async (file) => {
// 임의번호 생성
let randomNumber: string = '';
for (let i = 0; i < 8; i++) {
randomNumber += String(Math.floor(Math.random() * 10));
}
// 확장자 검사
const extension = path.extname(file.originalname).toLowerCase();
if (!allowedExtensions.includes(extension)) {
throw new BadRequestException(
'허용된 확장자가 아닙니다. (.png, .jpg, .jpeg, .bmp, .gif)'
);
}
const imageName = `ticketing/${date}_${randomNumber}`;
const ext = file.originalname.split('.').pop();
const imageUrl = await this.imageUploadToS3(`${imageName}.${ext}`, file, ext);
imageUrls.push({ imageUrl });
})
);
return imageUrls;
}
// 트랜젝션 실패 시 S3에 등록된 이미지 롤백
async rollbackS3Image(images: string[]) {
for (const image of images) {
const existingImageKey = await this.extractKeyFromUrl(image);
if (existingImageKey) {
await this.deleteImage(existingImageKey);
}
}
}
// URL에서 S3 Key 추출
async extractKeyFromUrl(url: string) {
const urlParts = url.split('/');
// URL의 마지막 부분이 key값
const key = urlParts.slice(3).join('/');
return key;
}
// S3에 등록된 이미지 삭제
async deleteImage(key: string) {
try {
const params = {
Bucket: this.configService.get('AWS_BUCKET'),
Key: key,
};
// S3에 접근해서 해당 이미지 객체 삭제
await this.s3.deleteObject(params);
} catch (err) {
console.log(err);
throw new InternalServerErrorException();
}
}
}
Nodemail를 이용한 이메일 인증 API 구현
인증 코드를 Body로 입력 받아서 유효한지 검사
인증 코드는 한 번 사용하면 필요 없기 때문에 데이터베이스가 아니라 Redis를 활용할 예정
이미지 업로드하는 코드는 예전 프로젝트에서 해봤기 때문에 간단했음
기존에 Insomnia에서 Multipart를 사용하는 방식이 아니라 Swagger에서 이미지를 업로드하는 방법으로 변경함
그래서 그에 필요한 데코레이터 설정들과 커스텀 데코레이터를 만들어서 구현함
참고자료