Skip to content

Commit 19098c7

Browse files
committed
feat: 카카오/구글 로그인 v2 추가
1 parent a0a55b6 commit 19098c7

File tree

3 files changed

+268
-16
lines changed

3 files changed

+268
-16
lines changed

src/auth/oauth.v2.controller.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Body, Controller, Post } from '@nestjs/common';
2+
import {
3+
ApiBadRequestResponse,
4+
ApiOkResponse,
5+
ApiOperation,
6+
ApiTags,
7+
ApiUnauthorizedResponse,
8+
} from '@nestjs/swagger';
9+
import { LoginOutput } from './dtos/login.dto';
10+
import { ErrorOutput } from '../common/dtos/output.dto';
11+
import { OAuthLoginRequest } from './dtos/request/oauth-login.request.dto';
12+
import { OAuthV2Service } from './oauth.v2.service';
13+
14+
@Controller('oauth/v2')
15+
@ApiTags('oauth v2')
16+
export class OauthV2Controller {
17+
constructor(private readonly oauthService: OAuthV2Service) {}
18+
19+
@ApiOperation({
20+
summary: '카카오 로그인',
21+
description:
22+
'카카오 로그인 메서드. (회원가입이 안되어 있으면 회원가입 처리 후 로그인 처리)',
23+
})
24+
@ApiOkResponse({
25+
description: '로그인 성공 여부와 함께 access, refresh token을 반환한다.',
26+
type: LoginOutput,
27+
})
28+
@ApiBadRequestResponse({
29+
description:
30+
'카카오 로그인 요청 시 발생하는 에러를 알려준다.(ex : email 제공에 동의하지 않은 경우)',
31+
type: ErrorOutput,
32+
})
33+
@ApiUnauthorizedResponse({
34+
description: '카카오 로그인 실패 여부를 알려준다.',
35+
type: ErrorOutput,
36+
})
37+
@Post('kakao')
38+
async kakaoOauth(
39+
@Body() oauthRequest: OAuthLoginRequest,
40+
): Promise<LoginOutput> {
41+
return this.oauthService.kakaoOauth(oauthRequest);
42+
}
43+
44+
@ApiOperation({
45+
summary: '구글 로그인 v2',
46+
description:
47+
'id token을 사용하는 구글 로그인 메서드. (회원가입이 안되어 있으면 회원가입 처리 후 로그인 처리)',
48+
})
49+
@ApiOkResponse({
50+
description: '로그인 성공 여부와 함께 access, refresh token을 반환한다.',
51+
type: LoginOutput,
52+
})
53+
@ApiBadRequestResponse({
54+
description: 'code가 잘못된 경우',
55+
type: ErrorOutput,
56+
})
57+
@Post('google')
58+
async googleAuthRedirect(
59+
@Body() oauthRequest: OAuthLoginRequest,
60+
): Promise<LoginOutput> {
61+
return this.oauthService.googleOauth(oauthRequest);
62+
}
63+
64+
// @ApiOperation({
65+
// summary: '애플 로그인',
66+
// description:
67+
// '애플 로그인 메서드. (회원가입이 안되어 있으면 회원가입 처리 후 로그인 처리)',
68+
// })
69+
// @Get('apple-login')
70+
// async appleLogin(
71+
// @Body() oauthRequest: OAuthLoginRequest,
72+
// ): Promise<LoginOutput> {
73+
// return this.oauthService.appleLogin(oauthRequest);
74+
// }
75+
}

src/auth/oauth.v2.service.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import {
2+
BadRequestException,
3+
Injectable,
4+
InternalServerErrorException,
5+
UnauthorizedException,
6+
} from '@nestjs/common';
7+
import { LoginOutput } from './dtos/login.dto';
8+
import { PROVIDER } from '../users/constant/provider.constant';
9+
import { User } from '../users/entities/user.entity';
10+
import { OAuthUtil } from './util/oauth.util';
11+
import { UserRepository } from '../users/repository/user.repository';
12+
import { Payload } from './jwt/jwt.payload';
13+
import { REFRESH_TOKEN_KEY } from './constants';
14+
import { refreshTokenExpirationInCache } from './auth.module';
15+
import { customJwtService } from './jwt/jwt.service';
16+
import { RedisService } from '../infra/redis/redis.service';
17+
import * as CryptoJS from 'crypto-js';
18+
import { CategoryRepository } from '../categories/category.repository';
19+
import { OAuthLoginRequest } from './dtos/request/oauth-login.request.dto';
20+
import { OAuth2Client } from 'google-auth-library';
21+
import { ConfigService } from '@nestjs/config';
22+
23+
@Injectable()
24+
export class OAuthV2Service {
25+
constructor(
26+
private readonly jwtService: customJwtService,
27+
private readonly userRepository: UserRepository,
28+
private readonly oauthUtil: OAuthUtil,
29+
private readonly categoryRepository: CategoryRepository,
30+
private readonly redisService: RedisService,
31+
private readonly configService: ConfigService,
32+
) {}
33+
34+
private readonly googleClient = new OAuth2Client(
35+
this.configService.get('GOOGLE_SECRET'),
36+
);
37+
38+
// OAuth Login
39+
async oauthLogin(email: string, provider: PROVIDER): Promise<LoginOutput> {
40+
try {
41+
const user: User = await this.userRepository.findOneByOrFail({
42+
email,
43+
provider,
44+
});
45+
if (user) {
46+
const payload: Payload = this.jwtService.createPayload(
47+
user.email,
48+
true,
49+
user.id,
50+
);
51+
const refreshToken = this.jwtService.generateRefreshToken(payload);
52+
await this.redisService.set(
53+
`${REFRESH_TOKEN_KEY}:${user.id}`,
54+
refreshToken,
55+
refreshTokenExpirationInCache,
56+
);
57+
58+
return {
59+
access_token: this.jwtService.sign(payload),
60+
refresh_token: refreshToken,
61+
};
62+
} else {
63+
throw new UnauthorizedException('Error in OAuth login');
64+
}
65+
} catch (e) {
66+
throw e;
67+
}
68+
}
69+
70+
/*
71+
* Get user info from Kakao Auth Server then create account,
72+
* login and return access token and refresh token
73+
*/
74+
async kakaoOauth({
75+
authorizationToken,
76+
}: OAuthLoginRequest): Promise<LoginOutput> {
77+
try {
78+
const { userInfo } = await this.oauthUtil.getKakaoUserInfo(
79+
authorizationToken,
80+
);
81+
82+
const email = userInfo.kakao_account.email;
83+
if (!email) {
84+
throw new BadRequestException('Please Agree to share your email');
85+
}
86+
87+
const user = await this.userRepository.findOneByEmailAndProvider(
88+
email,
89+
PROVIDER.KAKAO,
90+
);
91+
if (user) {
92+
return this.oauthLogin(user.email, PROVIDER.KAKAO);
93+
}
94+
95+
// 회원가입인 경우 기본 카테고리 생성 작업 진행
96+
const newUser = User.of({
97+
email,
98+
name: userInfo.kakao_account.profile.nickname,
99+
profileImage: userInfo.kakao_account.profile?.profile_image_url,
100+
password: this.encodePasswordFromEmail(email, process.env.KAKAO_JS_KEY),
101+
provider: PROVIDER.KAKAO,
102+
});
103+
104+
await this.userRepository.createOne(newUser);
105+
await this.categoryRepository.createDefaultCategories(newUser);
106+
107+
return this.oauthLogin(newUser.email, PROVIDER.KAKAO);
108+
} catch (e) {
109+
throw e;
110+
}
111+
}
112+
113+
// Login with Google account info
114+
async googleOauth({
115+
authorizationToken,
116+
}: OAuthLoginRequest): Promise<LoginOutput> {
117+
const ticket = await this.googleClient.verifyIdToken({
118+
idToken: authorizationToken,
119+
});
120+
const payload = ticket.getPayload();
121+
122+
if (!payload || !payload.name || !payload.email) {
123+
throw new BadRequestException('Invalid google payload');
124+
}
125+
126+
const user = await this.userRepository.findOneByEmailAndProvider(
127+
payload.email,
128+
PROVIDER.GOOGLE,
129+
);
130+
131+
if (user) {
132+
return this.oauthLogin(user.email, PROVIDER.GOOGLE);
133+
}
134+
135+
// 회원가입인 경우 기본 카테고리 생성 작업 진행
136+
const newUser = User.of({
137+
email: payload.email,
138+
name: payload.name,
139+
profileImage: payload.picture,
140+
password: this.encodePasswordFromEmail(
141+
payload.email,
142+
process.env.GOOGLE_CLIENT_ID,
143+
),
144+
provider: PROVIDER.GOOGLE,
145+
});
146+
147+
await this.userRepository.createOne(newUser);
148+
await this.categoryRepository.createDefaultCategories(newUser);
149+
150+
return this.oauthLogin(newUser.email, PROVIDER.GOOGLE);
151+
}
152+
153+
private encodePasswordFromEmail(email: string, key?: string): string {
154+
return CryptoJS.SHA256(email + key).toString();
155+
}
156+
157+
public async appleLogin(code: string) {
158+
const data = await this.oauthUtil.getAppleToken(code);
159+
160+
if (!data.id_token) {
161+
throw new InternalServerErrorException(
162+
`No token: ${JSON.stringify(data)}`,
163+
);
164+
}
165+
166+
const { sub: id, email } = this.jwtService.decode(data.id_token);
167+
168+
const user = await this.userRepository.findOneByEmailAndProvider(
169+
email,
170+
PROVIDER.APPLE,
171+
);
172+
173+
if (user) {
174+
return this.oauthLogin(user.email, PROVIDER.APPLE);
175+
}
176+
177+
const newUser = User.of({
178+
email,
179+
name: email.split('@')[0],
180+
password: this.encodePasswordFromEmail(
181+
email,
182+
process.env.APPLE_CLIENT_ID,
183+
),
184+
provider: PROVIDER.APPLE,
185+
});
186+
187+
await this.userRepository.createOne(newUser);
188+
await this.categoryRepository.createDefaultCategories(newUser);
189+
190+
return this.oauthLogin(newUser.email, PROVIDER.APPLE);
191+
}
192+
}

src/common/logger.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as winston from 'winston';
2-
import * as DailyRotateFile from 'winston-daily-rotate-file';
32

43
const { combine, label, printf, colorize } = winston.format;
54
const logFormat = printf(({ level, label, message }) => {
@@ -27,21 +26,7 @@ export const logger = winston.createLogger({
2726
level: 'info',
2827
levels: custom_level.levels,
2928
format: combine(colorize(), label({ label: 'Quickchive' }), logFormat),
30-
transports: [
31-
new winston.transports.Console(),
32-
new DailyRotateFile({
33-
filename: 'errors-%DATE%.log',
34-
datePattern: 'YYYY-MM-DD',
35-
maxSize: '1024',
36-
level: 'error',
37-
}),
38-
new DailyRotateFile({
39-
filename: '%DATE%.log',
40-
datePattern: 'YYYY-MM-DD',
41-
maxSize: '1024',
42-
level: 'info',
43-
}),
44-
],
29+
transports: [new winston.transports.Console()],
4530
});
4631

4732
export function getKoreaTime(): Date {

0 commit comments

Comments
 (0)