Skip to content

Commit 10bafd0

Browse files
authored
[hotfix] SDK 소셜로그인 변경 (moduwa-aac#62)
2 parents 4a8b28d + f2b0995 commit 10bafd0

File tree

6 files changed

+430
-40
lines changed

6 files changed

+430
-40
lines changed

docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ services:
7878
dockerfile: Dockerfile
7979
container_name: moduwa_backend
8080
restart: always
81+
dns:
82+
- 8.8.8.8
83+
- 8.8.4.4
8184
env_file:
8285
- .env
8386
environment:
@@ -88,6 +91,7 @@ services:
8891
FASTAPI_URL: http://fastapi:8000
8992
NODE_ENV: ${NODE_ENV:-development}
9093
PORT: ${PORT:-3000}
94+
APP_DEEP_LINK: ${APP_DEEP_LINK:-myapp}
9195
ports:
9296
- "3000:3000"
9397
volumes:

src/auth/controllers/guest.controller.js

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ import asyncHandler from '../../common/utils/asyncHandler.js';
22
import * as authService from '../services/auth.service.js';
33
import { verifyToken } from '../services/jwt.service.js';
44
import { getRefreshToken, deleteRefreshToken, addToBlacklist } from '../services/token.service.js';
5-
import { AuthResponseDto, TokenRefreshResponseDto } from '../dto/response/auth.response.js';
65
import { UserDetailResponseDto, ConvertAccountResponseDto } from '../dto/response/user.response.js';
76
import * as createGuestDto from '../dto/request/createGuest.dto.js';
87
import * as convertToSocialDto from '../dto/request/convertToSocial.dto.js';
9-
import * as refreshTokenDto from '../dto/request/refreshToken.dto.js';
10-
import { setRefreshTokenCookie, clearRefreshTokenCookie } from '../../utils/cookie.helper.js';
8+
import { clearRefreshTokenCookie } from '../../utils/cookie.helper.js';
119
import { InvalidRefreshTokenError } from '../../errors/app.error.js';
1210

1311
/**
@@ -18,10 +16,6 @@ export const createGuest = asyncHandler(async (req, res) => {
1816
const validatedData = createGuestDto.validate(req.body);
1917
const result = await authService.createGuestAccount(validatedData.deviceId);
2018

21-
// refreshToken은 httpOnly 쿠키로 설정
22-
setRefreshTokenCookie(res, result.tokens.refreshToken);
23-
24-
// 응답에는 accessToken만 포함
2519
const responseDto = {
2620
user: {
2721
id: result.user.id,
@@ -30,6 +24,7 @@ export const createGuest = asyncHandler(async (req, res) => {
3024
},
3125
tokens: {
3226
accessToken: result.tokens.accessToken,
27+
refreshToken: result.tokens.refreshToken,
3328
tokenType: 'Bearer',
3429
expiresIn: result.tokens.expiresIn
3530
}
@@ -63,28 +58,27 @@ export const convertToSocial = asyncHandler(async (req, res) => {
6358
* POST /api/auth/refresh
6459
*/
6560
export const refreshToken = asyncHandler(async (req, res) => {
66-
// 쿠키에서 refreshToken 읽기
67-
const refreshTokenFromCookie = req.cookies.refreshToken;
61+
// body 또는 쿠키에서 refreshToken 읽기 (SDK 방식은 body, 웹은 쿠키)
62+
const tokenFromBody = req.body?.refreshToken;
63+
const tokenFromCookie = req.cookies?.refreshToken;
64+
const incomingRefreshToken = tokenFromBody || tokenFromCookie;
6865

69-
if (!refreshTokenFromCookie) {
66+
if (!incomingRefreshToken) {
7067
throw new InvalidRefreshTokenError('Refresh token not found');
7168
}
7269

73-
const decoded = verifyToken(refreshTokenFromCookie);
70+
const decoded = verifyToken(incomingRefreshToken);
7471
const storedToken = await getRefreshToken(decoded.userId);
7572

76-
if (!storedToken || storedToken !== refreshTokenFromCookie) {
73+
if (!storedToken || storedToken !== incomingRefreshToken) {
7774
throw new InvalidRefreshTokenError();
7875
}
7976

8077
const tokens = await authService.generateTokens(decoded.userId, decoded.accountType);
8178

82-
// 새로운 refreshToken을 쿠키로 설정
83-
setRefreshTokenCookie(res, tokens.refreshToken);
84-
85-
// accessToken만 응답
8679
const responseDto = {
8780
accessToken: tokens.accessToken,
81+
refreshToken: tokens.refreshToken,
8882
tokenType: 'Bearer',
8983
expiresIn: tokens.expiresIn
9084
};
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import axios from 'axios';
2+
import asyncHandler from '../../common/utils/asyncHandler.js';
3+
import * as authService from '../services/auth.service.js';
4+
import { savePendingSignup } from '../services/token.service.js';
5+
import { ValidationError } from '../../errors/app.error.js';
6+
7+
/**
8+
* 카카오 SDK 로그인
9+
* POST /api/auth/kakao/sdk
10+
* Body: { kakaoAccessToken }
11+
*/
12+
export const kakaoSdkLogin = asyncHandler(async (req, res) => {
13+
const { kakaoAccessToken } = req.body;
14+
15+
if (!kakaoAccessToken) {
16+
throw new ValidationError('kakaoAccessToken이 필요합니다');
17+
}
18+
19+
// 카카오 API로 사용자 정보 조회
20+
let kakaoUser;
21+
try {
22+
const response = await axios.get('https://kapi.kakao.com/v2/user/me', {
23+
headers: { Authorization: `Bearer ${kakaoAccessToken}` }
24+
});
25+
kakaoUser = response.data;
26+
} catch (err) {
27+
throw new ValidationError('유효하지 않은 카카오 토큰입니다');
28+
}
29+
30+
const profile = {
31+
id: String(kakaoUser.id),
32+
email: kakaoUser.kakao_account?.email || null,
33+
nickname: kakaoUser.kakao_account?.profile?.nickname || null
34+
};
35+
36+
const existingUser = await authService.checkExistingUser('KAKAO', profile.id);
37+
38+
if (existingUser) {
39+
const result = await authService.loginExistingUser(existingUser);
40+
return res.success({
41+
user: {
42+
id: result.user.id,
43+
nickname: result.user.nickname,
44+
accountType: result.user.accountType
45+
},
46+
tokens: {
47+
accessToken: result.tokens.accessToken,
48+
refreshToken: result.tokens.refreshToken,
49+
tokenType: 'Bearer',
50+
expiresIn: result.tokens.expiresIn
51+
}
52+
}, '카카오 로그인 성공');
53+
}
54+
55+
// 신규 가입자 → pendingToken 발급
56+
const pendingToken = await savePendingSignup('KAKAO', profile);
57+
return res.status(200).success({ pendingToken, provider: 'KAKAO' }, '약관 동의가 필요합니다');
58+
});
59+
60+
/**
61+
* 구글 SDK 로그인
62+
* POST /api/auth/google/sdk
63+
* Body: { idToken }
64+
*/
65+
export const googleSdkLogin = asyncHandler(async (req, res) => {
66+
const { idToken } = req.body;
67+
68+
if (!idToken) {
69+
throw new ValidationError('idToken이 필요합니다');
70+
}
71+
72+
// 구글 tokeninfo API로 검증
73+
let googleUser;
74+
try {
75+
const response = await axios.get(`https://oauth2.googleapis.com/tokeninfo?id_token=${idToken}`);
76+
googleUser = response.data;
77+
} catch (err) {
78+
throw new ValidationError('유효하지 않은 구글 토큰입니다');
79+
}
80+
81+
const profile = {
82+
id: googleUser.sub,
83+
email: googleUser.email || null,
84+
nickname: googleUser.name || null
85+
};
86+
87+
const existingUser = await authService.checkExistingUser('GOOGLE', profile.id);
88+
89+
if (existingUser) {
90+
const result = await authService.loginExistingUser(existingUser);
91+
return res.success({
92+
user: {
93+
id: result.user.id,
94+
nickname: result.user.nickname,
95+
accountType: result.user.accountType
96+
},
97+
tokens: {
98+
accessToken: result.tokens.accessToken,
99+
refreshToken: result.tokens.refreshToken,
100+
tokenType: 'Bearer',
101+
expiresIn: result.tokens.expiresIn
102+
}
103+
}, '구글 로그인 성공');
104+
}
105+
106+
const pendingToken = await savePendingSignup('GOOGLE', profile);
107+
return res.status(200).success({ pendingToken, provider: 'GOOGLE' }, '약관 동의가 필요합니다');
108+
});
109+
110+
/**
111+
* 네이버 SDK 로그인
112+
* POST /api/auth/naver/sdk
113+
* Body: { naverAccessToken }
114+
*/
115+
export const naverSdkLogin = asyncHandler(async (req, res) => {
116+
const { naverAccessToken } = req.body;
117+
118+
if (!naverAccessToken) {
119+
throw new ValidationError('naverAccessToken이 필요합니다');
120+
}
121+
122+
// 네이버 API로 사용자 정보 조회
123+
let naverUser;
124+
try {
125+
const response = await axios.get('https://openapi.naver.com/v1/nid/me', {
126+
headers: { Authorization: `Bearer ${naverAccessToken}` }
127+
});
128+
naverUser = response.data.response;
129+
} catch (err) {
130+
throw new ValidationError('유효하지 않은 네이버 토큰입니다');
131+
}
132+
133+
const profile = {
134+
id: String(naverUser.id),
135+
email: naverUser.email || null,
136+
nickname: naverUser.nickname || naverUser.name || null
137+
};
138+
139+
const existingUser = await authService.checkExistingUser('NAVER', profile.id);
140+
141+
if (existingUser) {
142+
const result = await authService.loginExistingUser(existingUser);
143+
return res.success({
144+
user: {
145+
id: result.user.id,
146+
nickname: result.user.nickname,
147+
accountType: result.user.accountType
148+
},
149+
tokens: {
150+
accessToken: result.tokens.accessToken,
151+
refreshToken: result.tokens.refreshToken,
152+
tokenType: 'Bearer',
153+
expiresIn: result.tokens.expiresIn
154+
}
155+
}, '네이버 로그인 성공');
156+
}
157+
158+
const pendingToken = await savePendingSignup('NAVER', profile);
159+
return res.status(200).success({ pendingToken, provider: 'NAVER' }, '약관 동의가 필요합니다');
160+
});

src/auth/controllers/social.controller.js

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import { setRefreshTokenCookie } from '../../utils/cookie.helper.js';
1111
*/
1212
export const kakaoCallback = asyncHandler(async (req, res) => {
1313
passport.authenticate('kakao', { session: false }, async (err, profile) => {
14+
const appDeepLink = process.env.APP_DEEP_LINK;
1415
try {
1516
if (err || !profile) {
1617
console.error('Kakao auth error:', err);
17-
return res.redirect(`${process.env.CORS_ORIGIN}/login?error=kakao_auth_failed`);
18+
return res.redirect(`${appDeepLink}://login?error=kakao_auth_failed`);
1819
}
1920

2021
// 기존 회원인지 확인
@@ -28,8 +29,8 @@ export const kakaoCallback = asyncHandler(async (req, res) => {
2829
// refreshToken은 httpOnly 쿠키로 설정
2930
setRefreshTokenCookie(res, responseDto.tokens.refreshToken);
3031

31-
// accessToken과 userId만 URL로 전달
32-
const redirectUrl = `${process.env.CORS_ORIGIN}/auth/callback?` +
32+
// 앱 딥링크로 accessToken과 userId 전달
33+
const redirectUrl = `${appDeepLink}://auth/callback?` +
3334
`accessToken=${responseDto.tokens.accessToken}&` +
3435
`userId=${responseDto.user.id}`;
3536

@@ -39,15 +40,15 @@ export const kakaoCallback = asyncHandler(async (req, res) => {
3940
// 임시 토큰 생성 (5분 유효)
4041
const pendingToken = await savePendingSignup('KAKAO', profile);
4142

42-
const redirectUrl = `${process.env.CORS_ORIGIN}/terms?` +
43+
const redirectUrl = `${appDeepLink}://terms?` +
4344
`pendingToken=${pendingToken}&` +
4445
`provider=KAKAO`;
4546

4647
return res.redirect(redirectUrl);
4748
}
4849
} catch (error) {
4950
console.error('Kakao login error:', error);
50-
return res.redirect(`${process.env.CORS_ORIGIN}/login?error=login_failed`);
51+
return res.redirect(`${appDeepLink}://login?error=login_failed`);
5152
}
5253
})(req, res);
5354
});
@@ -57,10 +58,11 @@ export const kakaoCallback = asyncHandler(async (req, res) => {
5758
*/
5859
export const googleCallback = asyncHandler(async (req, res) => {
5960
passport.authenticate('google', { session: false }, async (err, profile) => {
61+
const appDeepLink = process.env.APP_DEEP_LINK;
6062
try {
6163
if (err || !profile) {
6264
console.error('Google auth error:', err);
63-
return res.redirect(`${process.env.CORS_ORIGIN}/login?error=google_auth_failed`);
65+
return res.redirect(`${appDeepLink}://login?error=google_auth_failed`);
6466
}
6567

6668
const existingUser = await authService.checkExistingUser('GOOGLE', profile.id);
@@ -69,27 +71,25 @@ export const googleCallback = asyncHandler(async (req, res) => {
6971
const result = await authService.loginExistingUser(existingUser);
7072
const responseDto = new AuthResponseDto(result.user, result.tokens);
7173

72-
// refreshToken은 httpOnly 쿠키로 설정
7374
setRefreshTokenCookie(res, responseDto.tokens.refreshToken);
7475

75-
// accessToken과 userId만 URL로 전달
76-
const redirectUrl = `${process.env.CORS_ORIGIN}/auth/callback?` +
76+
const redirectUrl = `${appDeepLink}://auth/callback?` +
7777
`accessToken=${responseDto.tokens.accessToken}&` +
7878
`userId=${responseDto.user.id}`;
7979

8080
return res.redirect(redirectUrl);
8181
} else {
8282
const pendingToken = await savePendingSignup('GOOGLE', profile);
8383

84-
const redirectUrl = `${process.env.CORS_ORIGIN}/terms?` +
84+
const redirectUrl = `${appDeepLink}://terms?` +
8585
`pendingToken=${pendingToken}&` +
8686
`provider=GOOGLE`;
8787

8888
return res.redirect(redirectUrl);
8989
}
9090
} catch (error) {
9191
console.error('Google login error:', error);
92-
return res.redirect(`${process.env.CORS_ORIGIN}/login?error=login_failed`);
92+
return res.redirect(`${appDeepLink}://login?error=login_failed`);
9393
}
9494
})(req, res);
9595
});
@@ -99,10 +99,11 @@ export const googleCallback = asyncHandler(async (req, res) => {
9999
*/
100100
export const naverCallback = asyncHandler(async (req, res) => {
101101
passport.authenticate('naver', { session: false }, async (err, profile) => {
102+
const appDeepLink = process.env.APP_DEEP_LINK;
102103
try {
103104
if (err || !profile) {
104105
console.error('Naver auth error:', err);
105-
return res.redirect(`${process.env.CORS_ORIGIN}/login?error=naver_auth_failed`);
106+
return res.redirect(`${appDeepLink}://login?error=naver_auth_failed`);
106107
}
107108

108109
const existingUser = await authService.checkExistingUser('NAVER', profile.id);
@@ -111,27 +112,25 @@ export const naverCallback = asyncHandler(async (req, res) => {
111112
const result = await authService.loginExistingUser(existingUser);
112113
const responseDto = new AuthResponseDto(result.user, result.tokens);
113114

114-
// refreshToken은 httpOnly 쿠키로 설정
115115
setRefreshTokenCookie(res, responseDto.tokens.refreshToken);
116116

117-
// accessToken과 userId만 URL로 전달
118-
const redirectUrl = `${process.env.CORS_ORIGIN}/auth/callback?` +
117+
const redirectUrl = `${appDeepLink}://auth/callback?` +
119118
`accessToken=${responseDto.tokens.accessToken}&` +
120119
`userId=${responseDto.user.id}`;
121120

122121
return res.redirect(redirectUrl);
123122
} else {
124123
const pendingToken = await savePendingSignup('NAVER', profile);
125124

126-
const redirectUrl = `${process.env.CORS_ORIGIN}/terms?` +
125+
const redirectUrl = `${appDeepLink}://terms?` +
127126
`pendingToken=${pendingToken}&` +
128127
`provider=NAVER`;
129128

130129
return res.redirect(redirectUrl);
131130
}
132131
} catch (error) {
133132
console.error('Naver login error:', error);
134-
return res.redirect(`${process.env.CORS_ORIGIN}/login?error=login_failed`);
133+
return res.redirect(`${appDeepLink}://login?error=login_failed`);
135134
}
136135
})(req, res);
137136
});

0 commit comments

Comments
 (0)