|
1 |
| -from typing import Any |
| 1 | +from typing import Any, Optional |
2 | 2 | from urllib.request import urlopen
|
3 | 3 |
|
4 | 4 | import requests
|
@@ -150,186 +150,151 @@ def post(self, request: Response, *args: Any, **kwargs: Any) -> Response:
|
150 | 150 | return Response({"message": "Email confirmation successful."}, status=status.HTTP_200_OK)
|
151 | 151 |
|
152 | 152 |
|
153 |
| -class KakaoLoginView(APIView): |
| 153 | +class OAuthLoginView(APIView): |
154 | 154 | permission_classes = [permissions.AllowAny]
|
155 | 155 |
|
| 156 | + def get_provider_info(self) -> dict[str, Any]: |
| 157 | + raise NotImplementedError |
| 158 | + |
156 | 159 | def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
|
157 |
| - code = request.data.get("code") # 프론트에서 보내준 코드 |
158 |
| - # 카카오 oauth 토큰 발급 url로 code가 담긴 post 요청을 보내 응답을 받는다. |
159 |
| - CLIENT_ID = settings.KAKAO_CLIENT_ID |
160 |
| - REDIRECT_URI = settings.REDIRECT_URI |
161 |
| - token_response = requests.post( |
162 |
| - "https://kauth.kakao.com/oauth/token", |
163 |
| - headers={"Content-Type": "application/x-www-form-urlencoded"}, |
164 |
| - data={ |
165 |
| - "grant_type": "authorization_code", |
166 |
| - "code": code, |
167 |
| - "redirect_uri": REDIRECT_URI, |
168 |
| - "client_id": CLIENT_ID, |
169 |
| - }, |
170 |
| - ) |
| 160 | + code = request.data.get("code") |
| 161 | + if not code: |
| 162 | + return Response({"msg": "인가코드가 필요합니다."}, status=status.HTTP_400_BAD_REQUEST) |
171 | 163 |
|
| 164 | + provider_info = self.get_provider_info() |
| 165 | + token_response = self.get_token(code, provider_info) |
172 | 166 | if token_response.status_code != status.HTTP_200_OK:
|
173 | 167 | return Response(
|
174 |
| - {"msg": "카카오 서버로 부터 토큰을 받아오는데 실패하였습니다."}, |
175 |
| - status=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| 168 | + {"msg": f"{provider_info['name']} 서버로 부터 토큰을 받아오는데 실패하였습니다."}, |
| 169 | + status=status.HTTP_400_BAD_REQUEST, |
176 | 170 | )
|
177 |
| - # 응답으로부터 액세스 토큰을 가져온다. |
178 |
| - access_token = token_response.json().get("access_token") |
179 |
| - response = requests.get( |
180 |
| - "https://kapi.kakao.com/v2/user/me", |
181 |
| - headers={ |
182 |
| - "Authorization": f"Bearer {access_token}", |
183 |
| - "Content-type": "application/x-www-form-urlencoded;charset=utf-8", |
184 |
| - }, |
185 |
| - ) |
186 | 171 |
|
187 |
| - if response.status_code != status.HTTP_200_OK: |
| 172 | + access_token = token_response.json().get("access_token") |
| 173 | + profile_response = self.get_profile(access_token, provider_info) |
| 174 | + if profile_response.status_code != status.HTTP_200_OK: |
188 | 175 | return Response(
|
189 |
| - {"msg": "카카오 서버로 부터 프로필 데이터를 받아오는데 실패하였습니다."}, |
190 |
| - status=status.HTTP_500_INTERNAL_SERVER_ERROR, |
191 |
| - ) |
192 |
| - response_data = response.json() |
193 |
| - kakao_account = response_data["kakao_account"] |
194 |
| - profile = kakao_account.get("profile") |
195 |
| - requests.post("https://kapi.kakao.com/v1/user/logout", headers={"Authorization": f"Bearer {access_token}"}) |
196 |
| - try: |
197 |
| - user = Account.objects.get(email=kakao_account.get("email")) |
198 |
| - access_token, refresh_token = jwt_encode(user) |
199 |
| - response = Response( |
200 |
| - { |
201 |
| - "access": str(access_token), |
202 |
| - "refresh": str(refresh_token), # type: ignore |
203 |
| - "email": user.email, |
204 |
| - "nickname": user.nickname, |
205 |
| - "profile_image": user.profile_img.url, |
206 |
| - }, |
207 |
| - status=status.HTTP_200_OK, |
208 |
| - ) |
209 |
| - # set_jwt_cookies(response, access_token, refresh_token) |
210 |
| - return response # type: ignore |
211 |
| - |
212 |
| - except Account.DoesNotExist: |
213 |
| - # 이미지를 다운로드하여 파일 객체로 가져옴 |
214 |
| - image_response = urlopen(profile.get("profile_image_url")) |
215 |
| - image_content = image_response.read() |
216 |
| - kakao_profile_image = ContentFile(image_content, name=f"kakao-profile-{uuid4_generator(8)}.jpg") |
217 |
| - user = Account.objects.create( |
218 |
| - email=kakao_account.get("email"), |
219 |
| - nickname=profile.get("nickname"), |
220 |
| - profile_img=kakao_profile_image, |
221 |
| - ) |
222 |
| - user.set_unusable_password() |
223 |
| - access_token, refresh_token = jwt_encode(user) |
224 |
| - response = Response( |
225 |
| - { |
226 |
| - "access": str(access_token), |
227 |
| - "refresh": str(refresh_token), # type: ignore |
228 |
| - "email": user.email, |
229 |
| - "nickname": user.nickname, |
230 |
| - "profile_image": user.profile_img.url, |
231 |
| - }, |
232 |
| - status=status.HTTP_200_OK, |
| 176 | + {"msg": f"{provider_info['name']} 서버로 부터 프로필 데이터를 받아오는데 실패하였습니다."}, |
| 177 | + status=status.HTTP_400_BAD_REQUEST, |
233 | 178 | )
|
234 |
| - # set_jwt_cookies(response, access_token, refresh_token) |
235 |
| - return response # type: ignore |
236 |
| - except Exception as e: |
237 |
| - return Response({"msg": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) |
238 |
| - |
239 |
| - |
240 |
| -class GoogleLoginView(APIView): |
241 |
| - permission_classes = [permissions.AllowAny] |
242 | 179 |
|
243 |
| - def post(self, request: Request) -> Response: |
244 |
| - code = request.data.get("code") |
245 |
| - |
246 |
| - client_id = settings.GOOGLE_CLIENT_ID |
247 |
| - client_secret = settings.GOOGLE_SECRET |
248 |
| - redirect_uri = settings.REDIRECT_URI |
249 |
| - |
250 |
| - if not code: |
251 |
| - return Response({"msg": "인가코드가 필요합니다."}, status=status.HTTP_400_BAD_REQUEST) |
| 180 | + return self.login_process_user(profile_response.json(), provider_info) |
252 | 181 |
|
253 |
| - # 인가코드를 통해 토큰을 가져오는 요청 |
254 |
| - token_req = requests.post( |
255 |
| - # f"https://oauth2.googleapis.com/token?client_id={client_id}&client_secret={client_secret}&code={code}&grant_type=authorization_code&redirect_uri={redirect_uri}" |
256 |
| - "https://oauth2.googleapis.com/token", |
257 |
| - headers={ |
258 |
| - "Content-type": "application/x-www-form-urlencoded;charset=utf-8", |
259 |
| - }, |
| 182 | + def get_token(self, code: str, provider_info: dict[str, Any]) -> requests.Response: |
| 183 | + return requests.post( |
| 184 | + provider_info["token_url"], |
| 185 | + headers={"Content-Type": "application/x-www-form-urlencoded"}, |
260 | 186 | data={
|
261 |
| - "client_id": client_id, |
262 |
| - "client_secret": client_secret, |
263 |
| - "code": code, |
264 | 187 | "grant_type": "authorization_code",
|
265 |
| - "redirect_uri": redirect_uri, |
| 188 | + "code": code, |
| 189 | + "redirect_uri": provider_info["redirect_uri"], |
| 190 | + "client_id": provider_info["client_id"], |
| 191 | + "client_secret": provider_info.get("client_secret"), |
266 | 192 | },
|
267 | 193 | )
|
268 |
| - # 요청의 응답을 json 파싱 |
269 |
| - token_req_json = token_req.json() |
270 |
| - if token_req_json.status_code != 200: |
271 |
| - return Response({"msg": token_req_json.get("error")}, status=status.HTTP_400_BAD_REQUEST) |
272 |
| - # 파싱된 데이터중 액세스 토큰을 가져옴 |
273 |
| - google_access_token = token_req_json.get("access_token") |
274 |
| - |
275 |
| - # 가져온 액세스토큰을 통해 사용자 정보에 접근하는 요청 |
276 |
| - info_response = requests.get( |
277 |
| - f"https://www.googleapis.com/oauth2/v1/userinfo?access_token={google_access_token}" |
| 194 | + |
| 195 | + def get_profile(self, access_token: str, provider_info: dict[str, Any]) -> requests.Response: |
| 196 | + return requests.get( |
| 197 | + provider_info["profile_url"], |
| 198 | + headers={ |
| 199 | + "Authorization": f"Bearer {access_token}", |
| 200 | + "Content-type": "application/x-www-form-urlencoded;charset=utf-8", |
| 201 | + }, |
278 | 202 | )
|
279 | 203 |
|
280 |
| - # 상태코드로 요청이 실패했는지 확인 |
281 |
| - if info_response.status_code != 200: |
282 |
| - return Response( |
283 |
| - {"message": "구글 api로부터 액세스토큰을 받아오는데 실패했습니다."}, |
284 |
| - status=status.HTTP_500_INTERNAL_SERVER_ERROR, |
285 |
| - ) |
| 204 | + def login_process_user(self, profile_res_data: dict[str, Any], provider_info: dict[str, Any]) -> Response: |
| 205 | + # 각 provider의 프로필 데이터 처리 로직 |
| 206 | + email = profile_res_data.get(provider_info["email_field"]) |
| 207 | + nickname = profile_res_data.get(provider_info["nickname_field"]) |
| 208 | + profile_img_url = profile_res_data.get(provider_info["profile_image_field"]) |
| 209 | + if provider_info["name"] == "네이버": |
| 210 | + profile_data = profile_res_data.get("response") |
| 211 | + if profile_data: |
| 212 | + email = profile_data.get(provider_info["email_field"]) |
| 213 | + nickname = profile_data.get(provider_info["nickname_field"]) |
| 214 | + profile_img_url = profile_data.get(provider_info["profile_image_field"]) |
| 215 | + elif provider_info["name"] == "카카오": |
| 216 | + account_data = profile_res_data.get("kakao_account") |
| 217 | + if account_data: |
| 218 | + email = account_data.get(provider_info["email_field"]) |
| 219 | + profile_data = account_data.get("profile") |
| 220 | + if profile_data: |
| 221 | + nickname = profile_data.get(provider_info["nickname_field"]) |
| 222 | + profile_img_url = profile_data.get(provider_info["profile_image_field"]) |
286 | 223 |
|
287 |
| - # 요청의 응답을 json 파싱 |
288 |
| - res_json = info_response.json() |
289 |
| - # 파싱된 데이터중 이메일값을 가져옴 |
290 |
| - email = res_json.get("email") |
291 |
| - # 파싱된 데이터중 닉네임을 가져옴 |
292 |
| - nickname = res_json.get("nickname") |
293 | 224 | try:
|
294 | 225 | user = Account.objects.get(email=email)
|
295 |
| - access_token, refresh_token = jwt_encode(user) |
296 |
| - response_data = { |
297 |
| - "access": str(access_token), |
298 |
| - "refresh": str(refresh_token), |
299 |
| - "email": user.email, |
300 |
| - "nickname": user.nickname, |
301 |
| - } |
302 |
| - if user.profile_img: |
303 |
| - response_data["profile_image"] = user.profile_img.url |
304 |
| - response = Response(response_data, status=status.HTTP_200_OK) |
305 |
| - # if api_settings.USE_JWT: |
306 |
| - # set_jwt_cookies(response, access_token, refresh_token) |
307 |
| - return response |
308 | 226 | except Account.DoesNotExist:
|
309 |
| - # 파싱된 데이터에서 프로필 이미지 url을 가져와서 파일로 변환 |
310 |
| - image_response = urlopen(res_json.get("picture")) |
| 227 | + user = self.create_user(email=email, nickname=nickname, profile_img_url=profile_img_url, provider_info=provider_info) # type: ignore |
| 228 | + |
| 229 | + access_token, refresh_token = jwt_encode(user) |
| 230 | + response_data = { |
| 231 | + "access": str(access_token), |
| 232 | + "refresh": str(refresh_token), |
| 233 | + "email": user.email, |
| 234 | + "nickname": user.nickname, |
| 235 | + } |
| 236 | + if user.profile_img: |
| 237 | + response_data["profile_image"] = user.profile_img.url |
| 238 | + return Response(response_data, status=status.HTTP_200_OK) |
| 239 | + |
| 240 | + def create_user( |
| 241 | + self, email: str, nickname: str, profile_img_url: Optional[str], provider_info: dict[str, Any] |
| 242 | + ) -> Account: |
| 243 | + if profile_img_url: |
| 244 | + image_response = urlopen(profile_img_url) |
311 | 245 | image_content = image_response.read()
|
312 |
| - google_profile_image = ContentFile(image_content, name=f"google-profile-{uuid4_generator(8)}.jpg") |
313 |
| - # 가져온 이메일, 닉네임, 프로필 이미지를 통해 유저 생성 |
314 |
| - user = Account.objects.create(email=email, nickname=nickname, profile_img=google_profile_image) |
315 |
| - user.set_unusable_password() |
316 |
| - access_token, refresh_token = jwt_encode(user) |
317 |
| - response_data = { |
318 |
| - "access": str(access_token), |
319 |
| - "refresh": str(refresh_token), |
320 |
| - "email": user.email, |
321 |
| - "nickname": user.nickname, |
322 |
| - } |
323 |
| - if user.profile_img: |
324 |
| - response_data["profile_image"] = user.profile_img.url |
325 |
| - response = Response(response_data, status=status.HTTP_200_OK) |
326 |
| - # if api_settings.USE_JWT: |
327 |
| - # set_jwt_cookies(response, access_token, refresh_token) |
328 |
| - return response |
329 |
| - |
330 |
| - except Exception as e: |
331 |
| - # 가입이 필요한 회원 |
332 |
| - return Response({"msg": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) |
| 246 | + profile_image = ContentFile(image_content, name=f"{provider_info['name']}-profile-{uuid4_generator(8)}.jpg") |
| 247 | + else: |
| 248 | + profile_image = None |
| 249 | + user = Account.objects.create(email=email, nickname=nickname, profile_img=profile_image) |
| 250 | + user.set_unusable_password() |
| 251 | + user.save() |
| 252 | + return user |
| 253 | + |
| 254 | + |
| 255 | +class KakaoLoginView(OAuthLoginView): |
| 256 | + def get_provider_info(self) -> dict[str, Any]: |
| 257 | + return { |
| 258 | + "name": "카카오", |
| 259 | + "redirect_uri": settings.KAKAO_REDIRECT_URI, |
| 260 | + "token_url": "https://kauth.kakao.com/oauth/token", |
| 261 | + "profile_url": "https://kapi.kakao.com/v2/user/me", |
| 262 | + "client_id": settings.KAKAO_CLIENT_ID, |
| 263 | + "client_secret": settings.KAKAO_CLIENT_SECRET, |
| 264 | + "email_field": "email", |
| 265 | + "nickname_field": "nickname", |
| 266 | + "profile_image_field": "profile_image_url", |
| 267 | + } |
| 268 | + |
| 269 | + |
| 270 | +class GoogleLoginView(OAuthLoginView): |
| 271 | + def get_provider_info(self) -> dict[str, Any]: |
| 272 | + return { |
| 273 | + "name": "구글", |
| 274 | + "redirect_uri": settings.GOOGLE_REDIRECT_URI, |
| 275 | + "token_url": "https://oauth2.googleapis.com/token", |
| 276 | + "profile_url": "https://www.googleapis.com/oauth2/v1/userinfo", |
| 277 | + "client_id": settings.GOOGLE_CLIENT_ID, |
| 278 | + "client_secret": settings.GOOGLE_SECRET, |
| 279 | + "email_field": "email", |
| 280 | + "nickname_field": "name", |
| 281 | + "profile_image_field": "picture", |
| 282 | + } |
| 283 | + |
| 284 | + |
| 285 | +class NaverLoginView(OAuthLoginView): |
| 286 | + def get_provider_info(self) -> dict[str, Any]: |
| 287 | + return { |
| 288 | + "name": "네이버", |
| 289 | + "redirect_uri": settings.NAVER_REDIRECT_URI, |
| 290 | + "token_url": "https://nid.naver.com/oauth2.0/token", |
| 291 | + "profile_url": "https://openapi.naver.com/v1/nid/me", |
| 292 | + "client_id": settings.NAVER_CLIENT_ID, |
| 293 | + "client_secret": settings.NAVER_CLIENT_SECRET, |
| 294 | + "email_field": "email", |
| 295 | + "nickname_field": "nickname", |
| 296 | + "profile_image_field": "profile_image", |
| 297 | + } |
333 | 298 |
|
334 | 299 |
|
335 | 300 | # class CustomConfirmEmailView(APIView):
|
|
0 commit comments