From 68451b70596681c044e837a1108295424284c1a3 Mon Sep 17 00:00:00 2001 From: Raymond <akdfhr2@gmail.com> Date: Sun, 31 Dec 2023 14:02:20 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EC=A0=9C=ED=95=9C=20=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/users/dtos/set-nickname.dto.ts | 5 ++- .../validator/max-length-byte.validator.ts | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/users/validator/max-length-byte.validator.ts diff --git a/src/users/dtos/set-nickname.dto.ts b/src/users/dtos/set-nickname.dto.ts index 73d1eef..667307e 100644 --- a/src/users/dtos/set-nickname.dto.ts +++ b/src/users/dtos/set-nickname.dto.ts @@ -1,9 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, MaxLength } from 'class-validator'; +import { IsString } from 'class-validator'; +import { MaxByteLength } from '../validator/max-length-byte.validator'; export class ChangeNicknameDTO { @ApiProperty({ description: '번경할 닉네임', default: '츄츄' }) @IsString() - @MaxLength(6) + @MaxByteLength(6) nickname: string; } diff --git a/src/users/validator/max-length-byte.validator.ts b/src/users/validator/max-length-byte.validator.ts new file mode 100644 index 0000000..43cc5f0 --- /dev/null +++ b/src/users/validator/max-length-byte.validator.ts @@ -0,0 +1,40 @@ +import { registerDecorator, ValidationOptions } from 'class-validator'; + +const LINE_FEED = 10; // '\n' + +export function getByteLength(decimal: number) { + return decimal >> 7 || LINE_FEED === decimal ? 2 : 1; +} + +export function getLimitedByteText(inputText: string, maxByte: number) { + const characters = inputText.split(''); + + return ( + characters.reduce((acc, cur) => { + const decimal = cur.charCodeAt(0); + const byte = getByteLength(decimal); // 글자 한 개가 몇 바이트 길이인지 구해주기 + return acc + byte; + }, 0) <= maxByte + ); +} + +export function MaxByteLength( + max: number, + validationOptions?: ValidationOptions, +) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'MaxByteLength', + target: object.constructor, + async: false, + propertyName: propertyName, + constraints: [], // * 아래 validate 의 args.contraints로 넘어감 + options: validationOptions, + validator: { + validate(value: any): boolean { + return getLimitedByteText(value, max); + }, + }, + }); + }; +} From 5b2d5290e09e2bad1f96c14f84717362ec7956ad Mon Sep 17 00:00:00 2001 From: Raymond <akdfhr2@gmail.com> Date: Sun, 31 Dec 2023 14:05:39 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=EC=B2=B4=ED=81=AC=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/users/dtos/set-nickname.dto.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/users/dtos/set-nickname.dto.ts b/src/users/dtos/set-nickname.dto.ts index 667307e..fa6ef33 100644 --- a/src/users/dtos/set-nickname.dto.ts +++ b/src/users/dtos/set-nickname.dto.ts @@ -5,6 +5,8 @@ import { MaxByteLength } from '../validator/max-length-byte.validator'; export class ChangeNicknameDTO { @ApiProperty({ description: '번경할 닉네임', default: '츄츄' }) @IsString() - @MaxByteLength(6) + @MaxByteLength(12, { + message: '닉네임이 너무 길어요.', + }) nickname: string; } From 748d81fa689d2ed2de585b13ee2caa9154410cac Mon Sep 17 00:00:00 2001 From: Raymond <akdfhr2@gmail.com> Date: Sun, 31 Dec 2023 17:13:49 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- global.d.ts | 2 + src/app.module.ts | 2 + src/oauth/dtos/service-provider.dto.ts | 1 + src/oauth/oauth.controller.ts | 16 ++++++- src/oauth/oauth.service.ts | 58 ++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/global.d.ts b/global.d.ts index 64be943..8b3dc67 100644 --- a/global.d.ts +++ b/global.d.ts @@ -10,6 +10,8 @@ declare namespace NodeJS { readonly GOOGLE_AUTH_CLIENT_ID: string; readonly GOOGLE_REDIRECT_URL: string; readonly OOGLE_AUTH_CLIENT_SECRET: string; + readonly KAKAO_CLIENT_ID: string; + readonly KAKAO_REDIRECT_URL: string; readonly SALT: string; readonly ROUND: string; readonly EXPIRESTOKEN: string; diff --git a/src/app.module.ts b/src/app.module.ts index 22b5ea2..59b7142 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,6 +22,8 @@ import { MailsModule } from './mails/mails.module'; GOOGLE_AUTH_CLIENT_ID: Joi.string().required(), GOOGLE_AUTH_CLIENT_SECRET: Joi.string().required(), GOOGLE_REDIRECT_URL: Joi.string().required(), + KAKAO_CLIENT_ID: Joi.string().required(), + KAKAO_REDIRECT_URL: Joi.string().required(), DB_HOST: Joi.string().required(), DB_PORT: Joi.string().required(), DB_USER: Joi.string().required(), diff --git a/src/oauth/dtos/service-provider.dto.ts b/src/oauth/dtos/service-provider.dto.ts index b0f3186..9d5a993 100644 --- a/src/oauth/dtos/service-provider.dto.ts +++ b/src/oauth/dtos/service-provider.dto.ts @@ -1,3 +1,4 @@ export enum ServiceProvider { GOOGLE = 'google', + KAKAO = 'kakao', } diff --git a/src/oauth/oauth.controller.ts b/src/oauth/oauth.controller.ts index 0031488..9123f1c 100644 --- a/src/oauth/oauth.controller.ts +++ b/src/oauth/oauth.controller.ts @@ -1,4 +1,12 @@ -import { Controller, Get, Param, Post, Query, Res } from '@nestjs/common'; +import { + BadRequestException, + Controller, + Get, + Param, + Post, + Query, + Res, +} from '@nestjs/common'; import { OauthService } from './oauth.service'; import { Response } from 'express'; import { @@ -40,11 +48,15 @@ export class OauthController { @Param('serviceName', new ParseExplicitEnumPipe(ServiceProvider)) serviceName: ServiceProvider, @Query('code') code: string, - // @Query('state') state: string, ): Promise<JWT> { switch (serviceName) { case ServiceProvider.GOOGLE: return await this.oauthService.userFromGoogle(code); + case ServiceProvider.KAKAO: + return await this.oauthService.userFromKakao(code); + default: + break; } + throw new BadRequestException(); } } diff --git a/src/oauth/oauth.service.ts b/src/oauth/oauth.service.ts index 0305b43..1f55872 100644 --- a/src/oauth/oauth.service.ts +++ b/src/oauth/oauth.service.ts @@ -35,6 +35,15 @@ export class OauthService { ); url.searchParams.set('access_type', 'offline'); return url; + case ServiceProvider.KAKAO: + const kakaoURL = new URL('https://kauth.kakao.com/oauth/authorize'); + kakaoURL.searchParams.set('client_id', process.env.KAKAO_CLIENT_ID); + kakaoURL.searchParams.set('response_type', 'code'); + kakaoURL.searchParams.set( + 'redirect_uri', + process.env.KAKAO_REDIRECT_URL, + ); + return kakaoURL; default: break; } @@ -79,4 +88,53 @@ export class OauthService { throw new BadRequestException('invalid request: ' + err?.message || ''); } } + async userFromKakao(code: string): Promise<JWT> { + try { + const form = new FormData(); + form.append('client_id', process.env.KAKAO_CLIENT_ID); + form.append('redirect_uri', process.env.KAKAO_REDIRECT_URL); + form.append('grant_type', 'authorization_code'); + form.append('code', code); + const response = await axios.post<{ + access_token: string; + token_type: string; + refresh_token: string; + expires_in: number; + scope: string; + refresh_token_expires_in: number; + }>('https://kauth.kakao.com/oauth/token ', form, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }); + + if (!response.data['access_token']) { + throw new BadRequestException('Access-Token을 받아오지 못 했습니다.'); + } + // 회원정보 가져오기 + const userUrl = 'https://kapi.kakao.com/v2/user/me'; + const userResponse = await axios.get(userUrl, { + params: { + access_token: response.data['access_token'], + }, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }); + + // const googleUesr = userResponse.data as GoogleUserInfo; + return userResponse.data; + // 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); + // return new JWT(token); + } catch (err) { + this.logger.error(err.message); + throw new BadRequestException('invalid request: ' + err?.message || ''); + } + } } From 4782dc1bf3376eb991b2dc2196df67201b5e2aa8 Mon Sep 17 00:00:00 2001 From: Raymond <akdfhr2@gmail.com> Date: Sun, 31 Dec 2023 17:25:05 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/oauth/oauth.service.ts | 87 ++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/src/oauth/oauth.service.ts b/src/oauth/oauth.service.ts index 1f55872..404b23e 100644 --- a/src/oauth/oauth.service.ts +++ b/src/oauth/oauth.service.ts @@ -1,14 +1,39 @@ -import { AuthService } from './../auth/auth.service'; -import { UserService } from './../users/user.service'; import { BadRequestException, Injectable, Logger } from '@nestjs/common'; -import axios from 'axios'; -import { GoogleUserInfo } from './dtos/google.dto'; -import { URL } from 'url'; import { InjectDataSource } from '@nestjs/typeorm'; -import { DataSource } from 'typeorm'; +import axios from 'axios'; +import { JWT } from 'src/auth/dtos/jwt.dto'; import { User } from 'src/users/entities/user.entity'; +import { DataSource } from 'typeorm'; +import { URL } from 'url'; +import { AuthService } from './../auth/auth.service'; +import { UserService } from './../users/user.service'; +import { GoogleUserInfo } from './dtos/google.dto'; import { ServiceProvider } from './dtos/service-provider.dto'; -import { JWT } from 'src/auth/dtos/jwt.dto'; + +export interface KakaoUser { + id: number; + connected_at: string; + properties: Properties; + kakao_account: KakaoAccount; +} + +export interface Properties { + nickname: string; +} + +export interface KakaoAccount { + profile_nickname_needs_agreement: boolean; + profile: Profile; + has_email: boolean; + email_needs_agreement: boolean; + is_email_valid: boolean; + is_email_verified: boolean; + email: string; +} + +export interface Profile { + nickname: string; +} @Injectable() export class OauthService { @@ -90,11 +115,6 @@ export class OauthService { } async userFromKakao(code: string): Promise<JWT> { try { - const form = new FormData(); - form.append('client_id', process.env.KAKAO_CLIENT_ID); - form.append('redirect_uri', process.env.KAKAO_REDIRECT_URL); - form.append('grant_type', 'authorization_code'); - form.append('code', code); const response = await axios.post<{ access_token: string; token_type: string; @@ -102,18 +122,27 @@ export class OauthService { expires_in: number; scope: string; refresh_token_expires_in: number; - }>('https://kauth.kakao.com/oauth/token ', form, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }>( + 'https://kauth.kakao.com/oauth/token ', + { + code, + grant_type: 'authorization_code', + client_id: process.env.KAKAO_CLIENT_ID, + redirect_uri: process.env.KAKAO_REDIRECT_URL, }, - }); + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); if (!response.data['access_token']) { throw new BadRequestException('Access-Token을 받아오지 못 했습니다.'); } // 회원정보 가져오기 const userUrl = 'https://kapi.kakao.com/v2/user/me'; - const userResponse = await axios.get(userUrl, { + const userResponse = await axios.get<KakaoUser>(userUrl, { params: { access_token: response.data['access_token'], }, @@ -121,17 +150,19 @@ export class OauthService { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', }, }); - - // const googleUesr = userResponse.data as GoogleUserInfo; - return userResponse.data; - // 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); - // return new JWT(token); + const kakaoUser = userResponse.data; + let user = await this.userService.findByUserEmail( + kakaoUser.kakao_account.email, + ); + if (!user) { + user = await this.userService.create({ + email: kakaoUser.kakao_account.email, + }); + } + const token = this.authService.sign(user.id); + user.refresh = token.refresh; + await this.dataSource.getRepository(User).save(user); + return new JWT(token); } catch (err) { this.logger.error(err.message); throw new BadRequestException('invalid request: ' + err?.message || ''); From 10d31e2d04392ae4a57bb693fb5865f6f84759be Mon Sep 17 00:00:00 2001 From: Raymond <akdfhr2@gmail.com> Date: Sun, 31 Dec 2023 17:30:14 +0900 Subject: [PATCH 5/5] 0.2.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 28bc77d..4ad8601 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shinnyang", - "version": "0.2.4", + "version": "0.2.5", "description": "", "author": { "name": "Medici",