From 96fcefdf988da16c9d50bc6a5e7b6b162bed4ba9 Mon Sep 17 00:00:00 2001 From: Raymond Date: Mon, 18 Dec 2023 02:11:25 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=ED=9A=8C=EC=9B=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원이 아닐 시, 생성 후 토큰 반환 --- src/oauth/oauth.service.ts | 5 ++++- src/users/user.service.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/oauth/oauth.service.ts b/src/oauth/oauth.service.ts index 23c281d..6c46294 100644 --- a/src/oauth/oauth.service.ts +++ b/src/oauth/oauth.service.ts @@ -40,7 +40,10 @@ export class OauthService { const googleUesr = userResponse.data as GoogleUserInfo; - const user = await this.userService.findByUserEmail(googleUesr.email); + let user = await this.userService.findByUserEmail(googleUesr.email); + if (!user) { + user = await this.userService.create({ email: googleUesr.email }); + } const token = this.authService.sign(user.id); user.refresh = token.refresh; await this.dataSource.getRepository(User).save(user); diff --git a/src/users/user.service.ts b/src/users/user.service.ts index 90d3401..1f4a06a 100644 --- a/src/users/user.service.ts +++ b/src/users/user.service.ts @@ -26,6 +26,14 @@ export class UserService { return createResponse(new PostUserResponseDto(result.generatedMaps[0].id)); } + async create(postUserRequestDto: PostUserRequestDto) { + const { email } = postUserRequestDto; + const repository = this.connection.getRepository(User); + const result = await repository.save(repository.create({ email })); + + return result; + } + async findByUserEmail(email: string) { const result = await this.connection .createQueryBuilder(User, 'users') From 07f8cc69d51a4430fa6cda08ba72c2b975a4a61f Mon Sep 17 00:00:00 2001 From: Raymond Date: Mon, 18 Dec 2023 02:40:56 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/dtos/jwt.dto.ts | 5 +-- src/oauth/dtos/google.dto.ts | 11 ++++--- src/users/dtos/set-nickname.dto.ts | 8 +++++ src/users/user.controller.ts | 23 +++++++++++++- src/users/user.module.ts | 3 +- src/users/user.repository.ts | 13 ++++++++ src/users/user.service.ts | 49 ++++++++++++++++++++++++++++-- 7 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 src/users/dtos/set-nickname.dto.ts diff --git a/src/auth/dtos/jwt.dto.ts b/src/auth/dtos/jwt.dto.ts index 6b13cf7..460a762 100644 --- a/src/auth/dtos/jwt.dto.ts +++ b/src/auth/dtos/jwt.dto.ts @@ -2,11 +2,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString } from 'class-validator'; export class JWT { - @ApiProperty() + @ApiProperty({ description: '엑세스 토큰' }) @IsString() access: string; - @ApiProperty() + @ApiProperty({ description: '리프레시 토큰' }) @IsString() refresh: string; @@ -17,6 +17,7 @@ export class JWT { } export class Payload { + @ApiProperty({ description: 'Guard를 통과한 후 사용자 아이디' }) @IsString() id: number; } diff --git a/src/oauth/dtos/google.dto.ts b/src/oauth/dtos/google.dto.ts index 9e08e9c..309acdf 100644 --- a/src/oauth/dtos/google.dto.ts +++ b/src/oauth/dtos/google.dto.ts @@ -5,26 +5,29 @@ import { UserResponse } from 'src/users/dtos/user.dto'; import { User } from 'src/users/entities/user.entity'; export class GoogleUserInfo { - @ApiProperty() + @ApiProperty({ description: '아이디' }) @IsString() id: string; - @ApiProperty() + @ApiProperty({ description: '이메일' }) @IsEmail() email: string; - @ApiProperty() + @ApiProperty({ description: '이메일 검증 여부' }) @IsBoolean() verified_email: boolean; - @ApiProperty() + @ApiProperty({ description: '프로필 사진 주소' }) @IsOptional() @IsString() picture: string; } export class GoogleAuthResponse { + @ApiProperty({ type: () => JWT }) token: JWT; + + @ApiProperty({ type: () => UserResponse }) user: UserResponse; constructor(token: JWT, user: User) { diff --git a/src/users/dtos/set-nickname.dto.ts b/src/users/dtos/set-nickname.dto.ts new file mode 100644 index 0000000..413268f --- /dev/null +++ b/src/users/dtos/set-nickname.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class ChangeNicknameDTO { + @ApiProperty({ description: '번경할 닉네임', default: '츄츄' }) + @IsString() + nickname: string; +} diff --git a/src/users/user.controller.ts b/src/users/user.controller.ts index 74dee13..6b72271 100644 --- a/src/users/user.controller.ts +++ b/src/users/user.controller.ts @@ -1,9 +1,10 @@ -import { Controller, Get, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; import { UserService } from './user.service'; import { AccessGuard } from 'src/auth/guards/acess.guard'; import { AuthUser } from 'src/auth/decorators/auth-user.decorator'; import { Payload } from 'src/auth/dtos/jwt.dto'; import { + ApiBearerAuth, ApiExtraModels, ApiOkResponse, ApiOperation, @@ -11,8 +12,10 @@ import { getSchemaPath, } from '@nestjs/swagger'; import { UserResponse } from './dtos/user.dto'; +import { ChangeNicknameDTO } from './dtos/set-nickname.dto'; @Controller('user') +@ApiBearerAuth() @ApiExtraModels(UserResponse) @ApiTags('User API') export class UserController { @@ -33,4 +36,22 @@ export class UserController { async getMe(@AuthUser() { id }: Payload) { return await this.userService.getMe(id); } + + @ApiOperation({ + summary: '닉네임 변경', + description: '나의 닉네임을 설정/변경한다.', + }) + @ApiOkResponse({ + description: '성공 여부', + type: Boolean, + }) + @ApiBearerAuth() + @UseGuards(AccessGuard) + @Post('nickname') + async changeNickname( + @AuthUser() { id }: Payload, + @Body() changeNicknameDTO: ChangeNicknameDTO, + ) { + return await this.userService.changeNickname(id, changeNicknameDTO); + } } diff --git a/src/users/user.module.ts b/src/users/user.module.ts index e680967..07ac7ce 100644 --- a/src/users/user.module.ts +++ b/src/users/user.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { UserController } from './user.controller'; import { UserService } from './user.service'; +import { UserRepository } from './user.repository'; @Module({ imports: [], controllers: [UserController], - providers: [UserService], + providers: [UserService, UserRepository], exports: [UserService], }) export class UserModule {} diff --git a/src/users/user.repository.ts b/src/users/user.repository.ts index e69de29..a7fa8e3 100644 --- a/src/users/user.repository.ts +++ b/src/users/user.repository.ts @@ -0,0 +1,13 @@ +import { DataSource, DeepPartial, Repository } from 'typeorm'; +import { User } from './entities/user.entity'; +import { InjectDataSource } from '@nestjs/typeorm'; + +export class UserRepository extends Repository { + constructor(@InjectDataSource() private readonly dataSource: DataSource) { + super(User, dataSource.manager); + } + + async updateAndReturning(userId: number, userLike: DeepPartial) { + return await this.update(userId, userLike); + } +} diff --git a/src/users/user.service.ts b/src/users/user.service.ts index 1f4a06a..051d52e 100644 --- a/src/users/user.service.ts +++ b/src/users/user.service.ts @@ -1,3 +1,4 @@ +import { UserRepository } from './user.repository'; import { UserResponse } from './dtos/user.dto'; import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; @@ -8,10 +9,14 @@ import { PostUserResponseDto, } from './dtos/create-users.dto'; import { createResponse } from 'src/utils/response.utils'; +import { ChangeNicknameDTO } from './dtos/set-nickname.dto'; @Injectable() export class UserService { - constructor(@InjectDataSource() private readonly connection: DataSource) {} + constructor( + @InjectDataSource() private readonly connection: DataSource, + private readonly userRepository: UserRepository, + ) {} async createUser(postUserRequestDto: PostUserRequestDto) { const { email } = postUserRequestDto; @@ -26,7 +31,15 @@ export class UserService { return createResponse(new PostUserResponseDto(result.generatedMaps[0].id)); } - async create(postUserRequestDto: PostUserRequestDto) { + /** + * 사용자를 생성하고 반환한다 + * + * @issue SNP-64 + * @author raymondanything + * @param {PostUserRequestDto} postUserRequestDto + * @returns {Promise} user + */ + async create(postUserRequestDto: PostUserRequestDto): Promise { const { email } = postUserRequestDto; const repository = this.connection.getRepository(User); const result = await repository.save(repository.create({ email })); @@ -43,10 +56,40 @@ export class UserService { return result; } - async getMe(id: number) { + /** + * 내정보를 조회한다 + * + * @issue SNP-64 + * @author raymondanything + * @param {number} id + * @returns {Promise} UserResponse + */ + async getMe(id: number): Promise { const user = await this.connection .getRepository(User) .findOne({ where: { id } }); return new UserResponse(user); } + + /** + * 닉네임을 설정 / 변경한다. + * + * @issue SNP-64 + * @link https://www.notion.so/raymondanything/SNP-64-Google-b3c69d93313d47fba51201412b70c635?pvs=4 + * @author raymondanything + * @param userId + * @param changeNicknameDTO + * @returns {Promise} + */ + async changeNickname( + userId: number, + changeNicknameDTO: ChangeNicknameDTO, + ): Promise { + try { + await this.userRepository.updateAndReturning(userId, changeNicknameDTO); + return true; + } catch (error) { + return false; + } + } } From 24623906abcdca4f4e545baebcf7938477955144 Mon Sep 17 00:00:00 2001 From: Raymond Date: Mon, 18 Dec 2023 02:57:24 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=20=EC=86=94=ED=8A=B8=20=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 012a33b..4d10ecc 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -14,7 +14,7 @@ export class AuthService { async refreshToken(refresh: string): Promise { this.verify(refresh, { - secret: process.env.RES_SALT, + secret: process.env.SALT, }); const queryRunner = this.dataSource.createQueryRunner();