diff --git a/requirements.txt b/requirements.txt index 641927a..8ae4bec 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/users/models.py b/users/models.py index 9f62dcd..ed643fd 100644 --- a/users/models.py +++ b/users/models.py @@ -11,7 +11,10 @@ from common.models.choiceModels import NotificationType class UserManager(BaseUserManager): def create_user(self, email, password, **kwargs): user = self.model(email = email, **kwargs) - user.set_password(password) + if not password: + user.set_unusable_password() + else: + user.set_password(password) user.save(using=self._db) return user diff --git a/users/serializers.py b/users/serializers.py index 817c753..315224f 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -2,6 +2,7 @@ from .models import * from rest_framework import serializers class JoinSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True, required=False) class Meta: model = User fields = [ @@ -20,7 +21,9 @@ class JoinSerializer(serializers.ModelSerializer): ] def create(self, validated_data): - return User.objects.create_user(**validated_data) + email = validated_data.pop('email', None) + password = validated_data.pop('password', None) + return User.objects.create_user(email=email, password=password, **validated_data) class SetPortofolioRequiredInfoSerializer(serializers.ModelSerializer): class Meta: diff --git a/users/urls.py b/users/urls.py index 3d84a25..2f386c8 100644 --- a/users/urls.py +++ b/users/urls.py @@ -9,6 +9,7 @@ urlpatterns = [ path('refresh-token/', RefreshAPIView.as_view()), path('join/', JoinAPIView.as_view()), path('login/', LoginAPIView.as_view()), + path('social-login/', GoogleLoginAPIView.as_view()), path('check/', CheckUserFieldDuplicateAPIView.as_view()), path('portfolio-info/', SetPortofolioRequiredInfoAPIView.as_view()), path('portfolio-info/check/', SetPortofolioRequiredInfoAPIView.as_view()), diff --git a/users/views.py b/users/views.py index d51fbe5..f8e557e 100644 --- a/users/views.py +++ b/users/views.py @@ -24,6 +24,9 @@ from portfolios.serializers import * from django.db import transaction +from google.oauth2 import id_token +from google.auth.transport import requests + class RefreshAPIView(APIView): permission_classes = [AllowAny] @@ -44,6 +47,65 @@ class RefreshAPIView(APIView): except TokenError as e: return Response({"message": f"Invalid token: {e}"}, status=status.HTTP_401_UNAUTHORIZED) +class GoogleLoginAPIView(APIView): + permission_classes = [AllowAny] + + @transaction.atomic + def post(self, request): + token_str = request.data.get("id_token") + if not token_str: + return Response({"message": "no id_token"}, status=status.HTTP_400_BAD_REQUEST) + + try: + idinfo = id_token.verify_oauth2_token( + token_str, + requests.Request(), + settings.GOOGLE_CLIENT_ID + ) + except ValueError: + return Response({"message": "wrong id_token"}, status=status.HTTP_400_BAD_REQUEST) + + email = idinfo["email"] + # sub = idinfo["sub"] # Google 고유 ID + + # 이미 소셜 회원가입을 한 경우, 로그인 기능. + if user := User.objects.filter(email=email).first(): + # 로그인 전 쿠키에 있는 리프레시 토큰 무효화 + try: + refresh = request.COOKIES.get('refresh') + if refresh: + refresh_token = RefreshToken(refresh) + refresh_token.blacklist() + except TokenError as e: + pass # 어차피 만료된거면 그냥 넘어가기 + + refresh = RefreshToken.for_user(user) + access = str(refresh.access_token) + + res = Response( + { + "message": "login success", + "user_id": user.id, + "nickname": user.nickname, + "access": access, + "is_member": True, + "is_custom_url": user.is_custom_url + }, + status=status.HTTP_200_OK, + ) + res.set_cookie("refresh", str(refresh), httponly=True, samesite=None, secure=not settings.DEBUG) + return res + else: + return Response( + { + "message": "Not member", + "email": email, + "is_member": False + }, + status=status.HTTP_200_OK + ) + + class JoinAPIView(APIView):