Skip to content

Commit

Permalink
[BE#210] 사용자 프로필 변경 및 조회 (#222)
Browse files Browse the repository at this point in the history
* feat: 유저 정보 설정시 이미지 S3에 저장

- auth.module에 jpg, jpeg, png 파일 필터링, 파일 크기 제한 10MB
- auth.module에 uuid를 통해 unique한 이미지로 저장되도록 설정
- auth.controller에 UseInterceptors(FileInterceptor)를 통해 Multer가 사용되도록 설정
- auth.controller에 UploadedFile 데코레이터를 통해 저장된 파일에 대한 정보를 불러옴
- auth.controller에 ApiConsumes를 통해 api문서에서 multipart/form-data라는걸 명시

* refactor: multer모듈 config 환경변수화

* feat: get info controller 작성

* fix: auth/info controller에서 반환 결과 수정
  • Loading branch information
yeongbinim authored Nov 27, 2023
1 parent 3758c42 commit d3c3d50
Show file tree
Hide file tree
Showing 11 changed files with 2,015 additions and 46 deletions.
1,889 changes: 1,871 additions & 18 deletions BE/package-lock.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions BE/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.456.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
Expand All @@ -32,12 +33,16 @@
"@nestjs/typeorm": "^10.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"D": "^1.0.0",
"multer": "^1.4.5-lts.1",
"multer-s3": "^3.0.1",
"mysql": "^2.18.1",
"nest-winston": "^1.9.4",
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"sharp": "^0.32.6",
"typeorm": "^0.3.17",
"uuid": "^9.0.1",
"winston": "^3.11.0"
Expand All @@ -48,9 +53,12 @@
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/multer": "^1.4.11",
"@types/multer-s3": "^3.0.3",
"@types/node": "^20.3.1",
"@types/passport-google-oauth20": "^2.0.14",
"@types/supertest": "^2.0.12",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
Expand Down
58 changes: 54 additions & 4 deletions BE/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AuthService } from './auth.service';

import {
Controller,
Get,
Expand All @@ -9,13 +8,16 @@ import {
Post,
Body,
Patch,
UseInterceptors,
UploadedFile,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
import { AccessTokenGuard } from './guard/bearer-token.guard';
import { User } from 'src/users/decorator/user.decorator';
import {
ApiBearerAuth,
ApiConsumes,
ApiExcludeEndpoint,
ApiOperation,
ApiResponse,
Expand All @@ -24,13 +26,18 @@ import {
import { UsersService } from 'src/users/users.service';
import { UpdateUserDto } from 'src/users/dto/update-user.dto';
import { UsersModel } from 'src/users/entity/users.entity';
import { FileInterceptor } from '@nestjs/platform-express';
import { ConfigService } from '@nestjs/config';
import * as path from 'path';
import { ENV } from 'src/common/const/env-keys.const';

@ApiTags('로그인 페이지')
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly usersService: UsersService,
private readonly configService: ConfigService,
) {}

@Get('google')
Expand Down Expand Up @@ -60,15 +67,58 @@ export class AuthController {

@Patch('info')
@UseGuards(AccessTokenGuard)
@UseInterceptors(FileInterceptor('image'))
@ApiOperation({ summary: '유저 정보 설정 (완)' })
@ApiConsumes('multipart/form-data')
@ApiResponse({ status: 200, description: '프로필 변경 성공' })
@ApiResponse({ status: 400, description: '잘못된 요청' })
@ApiResponse({ status: 401, description: '인증 실패' })
@ApiBearerAuth()
patchUser(
async patchUser(
@User('id') user_id: number,
@Body() user: UpdateUserDto,
): Promise<UsersModel> {
return this.usersService.updateUser(user_id, user);
@UploadedFile() file: S3UploadedFile,
): Promise<any> {
const image_url = file?.key;
const updatedUser = await this.usersService.updateUser(
user_id,
user as UsersModel,
image_url,
);
return {
nickname: updatedUser.nickname,
email: updatedUser.email,
image_url: path.join(
this.configService.get(ENV.CDN_ENDPOINT),
updatedUser.image_url,
),
};
}

@Get('info')
@UseGuards(AccessTokenGuard)
@ApiOperation({ summary: '유저 정보 설정 (완)' })
@ApiResponse({ status: 200, description: '프로필 조회 성공' })
@ApiResponse({ status: 400, description: '잘못된 요청' })
@ApiResponse({ status: 401, description: '인증 실패' })
@ApiBearerAuth()
async getUser(@User('id') user_id: number): Promise<any> {
const user = await this.usersService.findUserById(user_id);
return {
nickname: user.nickname,
email: user.email,
image_url: path.join(
this.configService.get(ENV.CDN_ENDPOINT),
user.image_url,
),
};
}
}

interface S3UploadedFile extends Express.Multer.File {
bucket: string;
key: string;
location: string;
etag: string;
versionId?: string;
}
8 changes: 7 additions & 1 deletion BE/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { AuthController } from './auth.controller';
import { UsersModule } from 'src/users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { GoogleStrategy } from './google.strategy';
import { MulterModule } from '@nestjs/platform-express';
import { multerConfig } from 'src/common/multer.config';

@Module({
imports: [
ConfigModule.forRoot(),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
Expand All @@ -17,6 +18,11 @@ import { GoogleStrategy } from './google.strategy';
signOptions: { expiresIn: configService.get('JWT_EXPIRES_IN') },
}),
}),
MulterModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: multerConfig,
}),
UsersModule,
],
controllers: [AuthController],
Expand Down
3 changes: 2 additions & 1 deletion BE/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersModel } from 'src/users/entity/users.entity';
import { UsersService } from 'src/users/users.service';

@Injectable()
Expand Down Expand Up @@ -49,7 +50,7 @@ export class AuthService {
id + Buffer.from(user.email + user.auth_type).toString('base64'),
email: user.email,
image_url: '',
};
} as UsersModel;
const newUser = await this.usersService.createUser(userEntity);
return {
access_token: this.signToken(newUser),
Expand Down
8 changes: 8 additions & 0 deletions BE/src/common/const/env-keys.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum ENV {
IMAGE_ENDPOINT = 'IMAGE_ENDPOINT',
IMAGE_ACCESSKEY = 'IMAGE_ACCESSKEY',
IMAGE_SECRETKEY = 'IMAGE_SECRETKEY',
IMAGE_REGION = 'IMAGE_REGION',
IMAGE_BUCKET = 'IMAGE_BUCKET',
CDN_ENDPOINT = 'CDN_ENDPOINT',
}
37 changes: 37 additions & 0 deletions BE/src/common/multer.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as path from 'path';
import * as multerS3 from 'multer-s3';
import { S3Client } from '@aws-sdk/client-s3';
import { v4 as uuid } from 'uuid';
import { BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { ENV } from './const/env-keys.const';

export const multerConfig = (configService: ConfigService): MulterOptions => ({
fileFilter: (req, file, callback) => {
const ext = path.extname(file.originalname);
if (ext !== '.jpg' && ext !== '.jpeg' && ext !== '.png') {
return callback(
new BadRequestException('jpg/jpeg/png 파일만 업로드 가능합니다!'),
false,
);
}
return callback(null, true);
},
limits: { fileSize: 1024 * 1024 * 10 }, //10MB
storage: multerS3({
s3: new S3Client({
endpoint: configService.get(ENV.IMAGE_ENDPOINT),
credentials: {
accessKeyId: configService.get(ENV.IMAGE_ACCESSKEY),
secretAccessKey: configService.get(ENV.IMAGE_SECRETKEY),
},
region: configService.get(ENV.IMAGE_REGION),
}),
bucket: configService.get(ENV.IMAGE_BUCKET),
key: function (req, file, callback) {
const fileExtension = path.extname(file.originalname);
callback(null, `IMG_${uuid()}${fileExtension}`);
},
}),
});
15 changes: 3 additions & 12 deletions BE/src/users/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,20 @@ import { PickType } from '@nestjs/mapped-types';
import { UsersModel } from '../entity/users.entity';
import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto extends PickType(UsersModel, [
'nickname',
'email',
'image_url',
]) {
export class CreateUserDto extends PickType(UsersModel, ['nickname', 'email']) {
@ApiProperty({
type: 'string',
example: '어린콩',
description: '닉네임',
required: false,
})
nickname: string;

@ApiProperty({
type: 'string',
example: 'https://sldkjfds/dsflkdsjf.png',
description: '이미지 링크',
})
image_url: string;

@ApiProperty({
type: 'string',
example: '[email protected]',
description: '이메일',
required: true,
})
email: string;
}
8 changes: 5 additions & 3 deletions BE/src/users/dto/update-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ export class UpdateUserDto extends PartialType(UsersModel) {
type: 'string',
example: '어린콩',
description: '닉네임',
required: false,
})
@IsOptional()
nickname?: string;

@ApiProperty({
type: 'string',
example: 'https://sldkjfds/dsflkdsjf.png',
description: '이미지 링크',
format: 'binary',
description: '이미지 파일',
required: false,
})
@IsOptional()
image_url?: string;
image?: Express.Multer.File;
}
5 changes: 3 additions & 2 deletions BE/src/users/entity/users.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { IsEmail, IsString, Length } from 'class-validator';
import { IsEmail, IsOptional, IsString, Length } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { StudyLogs } from 'src/study-logs/study-logs.entity';
import { Categories } from 'src/categories/categories.entity';
Expand Down Expand Up @@ -44,7 +44,8 @@ export class UsersModel {
@Column({
nullable: true,
})
image_url: string;
@IsOptional()
image_url?: string;

@Column({
type: 'enum',
Expand Down
22 changes: 17 additions & 5 deletions BE/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ConfigService } from '@nestjs/config';
import { v4 } from 'uuid';
import { GreenEyeResponse } from './interface/greeneye.interface';


@Injectable()
export class UsersService {
constructor(
Expand All @@ -16,12 +17,11 @@ export class UsersService {
private config: ConfigService,
) {}

async createUser(user: CreateUserDto): Promise<UsersModel> {
async createUser(user: UsersModel): Promise<UsersModel> {
try {
const userObject = this.usersRepository.create({
nickname: user.nickname,
email: user.email,
image_url: user.image_url,
});
return await this.usersRepository.save(userObject);
} catch (error) {
Expand All @@ -34,15 +34,19 @@ export class UsersService {
}
}

async updateUser(user_id: number, user: UpdateUserDto): Promise<UsersModel> {
async updateUser(
user_id: number,
user: UsersModel,
image_url?: string,
): Promise<UsersModel> {
const selectedUser = await this.usersRepository.findOne({
where: { id: user_id },
});
if (user.nickname) {
selectedUser.nickname = user.nickname;
}
if (user.image_url) {
selectedUser.image_url = user.image_url;
if (image_url) {
selectedUser.image_url = image_url;
}

const updatedUser = await this.usersRepository.save(selectedUser);
Expand All @@ -67,6 +71,13 @@ export class UsersService {
return selectedUser;
}

async findUserById(user_id: number): Promise<UsersModel> {
const selectedUser = await this.usersRepository.findOne({
where: { id: user_id },
});

return selectedUser;

async isNormalImage(image_url: string): Promise<boolean> {
const THRESHOLD = 0.5;
const response = await this.requestClovaGreenEye(image_url);
Expand Down Expand Up @@ -109,5 +120,6 @@ export class UsersService {
} catch (error) {
throw new BadRequestException('이미지 검사 요청 실패');
}

}
}

0 comments on commit d3c3d50

Please sign in to comment.