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

presigned url 적용 #147

Merged
merged 14 commits into from
Nov 30, 2023
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
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"class-validator": "^0.14.0",
"crypto-js": "^4.2.0",
"express": "^4.18.2",
"fluent-ffmpeg": "^2.1.2",
"lodash": "^4.17.21",
"mongoose": "^8.0.0",
"reflect-metadata": "^0.1.13",
Expand Down
2 changes: 1 addition & 1 deletion server/src/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class AppController {
/**
* 광고 이미지 응답
*/
@ApiTags('COMPLETE')
@ApiTags('LEGACY')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LEGACY 태그로 기존 코드 구분하신게 좋은거 같아요!

@Get('ads')
@ApiSuccessResponse(200, '광고 조회 성공', AdsResponseDto)
@ApiFailResponse('인증 실패', [InvalidTokenException, TokenExpiredException])
Expand Down
8 changes: 6 additions & 2 deletions server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { ActionModule } from './action/action.module';
import { UserModule } from './user/user.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PresignedUrlController } from './presigned-url/presigned-url.controller';
import { PresignedUrlService } from './presigned-url/presigned-url.service';
import { PresignedUrlModule } from './presigned-url/presigned-url.module';

@Module({
imports: [
Expand All @@ -16,8 +19,9 @@ import { AppService } from './app.service';
VideoModule,
ActionModule,
UserModule,
PresignedUrlModule,
],
controllers: [AppController],
providers: [AppService],
controllers: [AppController, PresignedUrlController],
providers: [AppService, PresignedUrlService],
})
export class AppModule {}
15 changes: 5 additions & 10 deletions server/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
Controller,
Post,
Body,
UseInterceptors,
UploadedFile,
} from '@nestjs/common';
import { Controller, Post, Body, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
import { ApiSuccessResponse } from 'src/decorators/api-succes-response';
Expand All @@ -21,7 +15,6 @@ import { SigninRequestDto } from './dto/signin-request.dto';
import { RefreshRequestDto } from './dto/refresh-request.dto';
import { RefreshResponseDto } from './dto/refresh-response.dto';

@ApiTags('COMPLETE')
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
Expand All @@ -30,22 +23,23 @@ export class AuthController {
* 회원가입
*/
@Post('signup')
@ApiTags('AUTH')
@ApiConsumes('multipart/form-data')
@ApiSuccessResponse(201, '회원가입 성공', SignupResponseDto)
@ApiFailResponse('인증 실패', [OAuthFailedException])
@ApiFailResponse('회원가입 실패', [UserConflictException])
@UseInterceptors(FileInterceptor('profileImage'))
signUp(
@UploadedFile() profileImage: Express.Multer.File,
@Body() signupRequestDto: SignupRequestDto,
): Promise<SignupResponseDto> {
return this.authService.create(signupRequestDto, profileImage);
return this.authService.create(signupRequestDto);
}

/**
* 로그인
*/
@Post('login')
@ApiTags('AUTH')
@ApiSuccessResponse(201, '로그인 성공', SigninResponseDto)
@ApiFailResponse('인증 실패', [LoginFailException, OAuthFailedException])
signin(
Expand All @@ -58,6 +52,7 @@ export class AuthController {
* 토큰 재발급
*/
@Post('refresh')
@ApiTags('AUTH')
@ApiSuccessResponse(201, '토큰 재발급 성공', RefreshResponseDto)
@ApiFailResponse('인증 실패', [InvalidRefreshTokenException])
refresh(
Expand Down
19 changes: 4 additions & 15 deletions server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { UserConflictException } from 'src/exceptions/conflict.exception';
import { putObject } from 'src/ncpAPI/putObject';
import { User, UserDocument } from 'src/user/schemas/user.schema';
import { JwtService } from '@nestjs/jwt';
import { LoginFailException } from 'src/exceptions/login-fail.exception';
Expand All @@ -23,24 +22,14 @@ export class AuthService {
private jwtService: JwtService,
) {}

async create(
signupRequestDto: SignupRequestDto,
profileImage: Express.Multer.File,
): Promise<SignupResponseDto> {
async create(signupRequestDto: SignupRequestDto): Promise<SignupResponseDto> {
const { uuid } = signupRequestDto;
if (await this.UserModel.findOne({ uuid })) {
throw new UserConflictException();
}
// TODO 프로필 이미지 예외처리
const profileImageExtension = profileImage
? profileImage.originalname.split('.').pop()
: null;
if (profileImage) {
putObject(
process.env.PROFILE_BUCKET,
`${uuid}.${profileImageExtension}`,
profileImage.buffer,
);
const profileImageExtension = signupRequestDto.profileExtension;
if (profileImageExtension) {
// TODO 프로필 이미지 업로드 됐는지 확인
}

const newUser = new this.UserModel({
Expand Down
32 changes: 32 additions & 0 deletions server/src/ncpAPI/presignedURL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { HttpRequest } from '@smithy/protocol-http';
import { S3RequestPresigner } from '@aws-sdk/s3-request-presigner';
import { parseUrl } from '@smithy/url-parser';
import { formatUrl } from '@aws-sdk/util-format-url';
import { Hash } from '@smithy/hash-node';

export const createPresignedUrl = async (
bucketName: string,
objectName: string,
method: string,
) => {
const region = 'kr-standard';
const endPoint = 'https://kr.object.ncloudstorage.com';
const canonicalURI = `/${bucketName}/${objectName}`;
const apiUrl = `${endPoint}${canonicalURI}`;
const url = parseUrl(apiUrl);

const presigner = new S3RequestPresigner({
credentials: {
accessKeyId: process.env.ACCESS_KEY,
secretAccessKey: process.env.SECRET_KEY,
},
region,
sha256: Hash.bind(null, 'sha256'),
});

const signedUrlObject = await presigner.presign(
new HttpRequest({ ...url, method }),
{ expiresIn: 100 },
);
return formatUrl(signedUrlObject);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class AdvertisementPresignedUrlRequestDto {
/**
* 특정 광고 이미지의 presigned url만 받고 싶은 경우
* @example 'test.webp'
Comment on lines +3 to +4
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

*/
name?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* eslint-disable max-classes-per-file */
import { ApiProperty } from '@nestjs/swagger';

class PresignedUrlDto {
/**
* 파일 이름
* @example 'test.webp'
*/
name: string;

/**
* 파일을 가져올 수 있는 presigned url
* @example "https://kr.object.ncloudstorage.com/"
*/
url: string;
}

export class AdvertisementPresignedUrlResponseDto {
@ApiProperty({ type: [PresignedUrlDto] })
advertisements: [PresignedUrlDto];
}
7 changes: 7 additions & 0 deletions server/src/presigned-url/dto/presigned-url-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class PresignedUrlResponseDto {
/**
* presigned url
* @example 'https://kr.object.ncloudstorage.com/'
*/
presignedUrl: string;
}
15 changes: 15 additions & 0 deletions server/src/presigned-url/dto/profile-presigned-url-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsUUID } from 'class-validator';

export class ProfilePresignedUrlRequestDto {
/**
* user uuid
*/
@IsUUID()
uuid: string;

/**
* 프로필 이미지 확장자
* @example 'webp'
*/
profileExtension: string;
}
21 changes: 21 additions & 0 deletions server/src/presigned-url/dto/reissue-presigned-url-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { IsEnum } from 'class-validator';

enum TypeEnum {
thumbnail = 'thumbnail',
profile = 'profile',
}

export class ReissuePresignedUrlRequestDto {
/**
* 요청하는 이미지 종류
* @example 'thumbnail'
*/
@IsEnum(TypeEnum)
type: TypeEnum;

/**
* 이미지 확장자
* @example 'webp'
*/
extension: string;
}
13 changes: 13 additions & 0 deletions server/src/presigned-url/dto/video-presigned-url-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class VIdeoPresignedUrlRequestDto {
/**
* 비디오 확장자
* @example 'mp4'
*/
videoExtension: string;

/**
* 썸네일 이미지 확장자
* @example 'webp'
*/
thumbnailExtension: string;
}
21 changes: 21 additions & 0 deletions server/src/presigned-url/dto/video-presigned-url-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Types } from 'mongoose';

export class VideoPresignedUrlResponseDto {
/**
* 비디오 ID
* @example '6567eb9aa1efacad06e24b81'
*/
videoId: Types.ObjectId;

/**
* 비디오 업로드 presigned url
* @example 'https://kr.object.ncloudstorage.com'
*/
videoUrl: string;

/**
* 썸네일 업로드 presigned url
* @example 'https://kr.object.ncloudstorage.com'
*/
thumbnailUrl: string;
}
81 changes: 81 additions & 0 deletions server/src/presigned-url/presigned-url.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiParam, ApiTags } from '@nestjs/swagger';
import { ApiFailResponse } from 'src/decorators/api-fail-response';
import { AuthGuard } from 'src/auth/auth.guard';
import { InvalidTokenException } from 'src/exceptions/invalid-token.exception';
import { TokenExpiredException } from 'src/exceptions/token-expired.exception';
import { ApiSuccessResponse } from 'src/decorators/api-succes-response';
import { PresignedUrlService } from './presigned-url.service';
import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presigned-url-request.dto';
import { ProfilePresignedUrlRequestDto } from './dto/profile-presigned-url-request.dto';
import { VIdeoPresignedUrlRequestDto } from './dto/video-presigned-url-request.dto';
import { ReissuePresignedUrlRequestDto } from './dto/reissue-presigned-url-request.dto';
import { AdvertisementPresignedUrlResponseDto } from './dto/advertisement-presigned-url-response.dto';
import { PresignedUrlResponseDto } from './dto/presigned-url-response.dto';
import { VideoPresignedUrlResponseDto } from './dto/video-presigned-url-response.dto';

@ApiTags('PRESIGNED URL')
@ApiBearerAuth()
@UseGuards(AuthGuard)
@ApiFailResponse('인증 실패', [InvalidTokenException, TokenExpiredException])
@Controller('presigned-url')
export class PresignedUrlController {
constructor(private presignedUrlService: PresignedUrlService) {}

/**
* 광고 이미지를 GET하는 url 발급
*/
@Get('advertisements')
@ApiSuccessResponse(
200,
'광고 이미지 가져오는 url 발급 성공',
AdvertisementPresignedUrlResponseDto,
)
getAdvertisementPresignedUrl(
@Query() query: AdvertisementPresignedUrlRequestDto,
) {
return this.presignedUrlService.getAdvertisementPresignedUrl(query.name);
}

/**
* 프로필 이미지를 PUT하는 url 발급
*/
@Get('profile')
@ApiSuccessResponse(
200,
'프로필 이미지를 업로드하는 url 발급 성공',
PresignedUrlResponseDto,
)
putProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) {
return this.presignedUrlService.putProfilePresignedUrl(query);
}

/**
* 비디오, 썸네일 이미지를 PUT하는 url 발급
*/
@Get('video')
@ApiSuccessResponse(
200,
'비디오/썸네일을 업로드하는 url 발급 성공',
VideoPresignedUrlResponseDto,
)
putVideoPresignedUrl(@Query() query: VIdeoPresignedUrlRequestDto) {
return this.presignedUrlService.putVideoPresignedUrl(query);
}

/**
* 만료된 presigned url 재발급
*/
@Get('reissue/:id')
@ApiParam({
name: 'id',
description: '썸네일 재발급 시 비디오ID, 프로필 재발급 시 유저 UUID',
})
@ApiSuccessResponse(200, 'presigned url 재발급 성공', PresignedUrlResponseDto)
getImagePresignedUrl(
@Param('id') id: string,
@Query() query: ReissuePresignedUrlRequestDto,
) {
return this.presignedUrlService.getImagePresignedUrl(id, query);
}
}
4 changes: 4 additions & 0 deletions server/src/presigned-url/presigned-url.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';

@Module({})
export class PresignedUrlModule {}
Loading
Loading