과제/개인과제

[Django] Todo List 만들기(2) - 회원 기능

마이구미+ 2023. 4. 27. 21:03

<회원가입>

  • 회원가입부터 너무나 많은 오류를 만났다
  • 그중 기억 남는 거 하나만 적는다..
  • 유저필드로는 이메일, 이름, 성별, 나이, 자기소개가 있는데, 회원가입 시 자기소개만 빼고 필수값으로 설정했다
  • 그랬더니 superuser를 만들 때 필요한 필드가 없다고 ,,,ㅜㅜ 
  • superuser는 그냥 이메일이랑 패스워드만 입력받아서 생성하고 싶은데...
  • 그리고 명령어 쓰면 어차피 이메일이랑 패스워드 쓰는 것밖에 안 나온다,,
  • 그래서 그냥 나머지 값들은 수동으로 적어놨다
class UserManager(BaseUserManager):
    def create_user(self, email, name, gender, age, introduction, password=None):
        ...

    def create_superuser(self, email, password=None):
        user = self.create_user(
            email=email,
            password=password,
            name="관리자",
            gender="non-choice",
            age="0",
            introduction="관리자 계정입니다"
        )
        ...
  • 이런 식으로 ㅋㅋㅋ
  • 여튼 이렇게 하니까 superuser도 잘 생성되고 일반 유저도 잘 가입된다

- models.py

# users/models.py

from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser

# 나이 최솟값, 최댓값을 설정하기 위해 import함
from django.core.validators import MinValueValidator, MaxValueValidator


class UserManager(BaseUserManager):
    def create_user(self, email, name, gender, age, introduction, password=None):
        if not email:
            raise ValueError("Users must have an email address")

        user = self.model(
            email=self.normalize_email(email),
            name=name,
            gender=gender,
            age=age,
            introduction=introduction,
        )
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None):
        user = self.create_user(
            email=email,
            password=password,
            name="관리자",
            gender="non-choice",
            age="0",
            introduction="관리자 계정입니다"
        )
        user.is_admin = True
        user.save(using=self._db)
        return user


class User(AbstractBaseUser):
    GENDER_CHOICES = (
        ("male", "남자"),
        ("female", "여자"),
        ("unknown", "모름"),
        ("non-choice", "선택하지 않음")
    )

    email = models.EmailField(
        verbose_name="email address",
        max_length=255,
        unique=True,
    )
    name = models.CharField("이름", max_length=20)
    gender = models.CharField("성별", max_length=20, choices=GENDER_CHOICES)
    age = models.IntegerField(
        "나이", validators=[MinValueValidator(0), MaxValueValidator(120)])
    introduction = models.TextField("자기소개", blank=True)
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = "email"
    # 필드에 blank=True, null=True를 하지 않으면 필수값이 되던데 이 부분이 왜 따로 있는지 궁금
    REQUIRED_FIELDS = []	

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        "Does the user have a specific permission?"
        # Simplest possible answer: Yes, always
        return True

    def has_module_perms(self, app_label):
        "Does the user have permissions to view the app `app_label`?"
        # Simplest possible answer: Yes, always
        return True

    @property
    def is_staff(self):
        "Is the user a member of staff?"
        # Simplest possible answer: All admins are staff
        return self.is_admin

- urls.py

# users/urls.py

from django.urls import path
from users import views


urlpatterns = [
    path('api/signup/', views.SignupView.as_view(), name='signup_view'),
]

- serializers.py

# users/serializers.py

from rest_framework import serializers
from users.models import User


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = "__all__"

    def create(self, validated_data):
        user = super().create(validated_data)
        password = user.password
        user.set_password(password)
        user.save()
        return user

- views.py

# users/views.py

from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from users.serializers import UserSerializer


class SignupView(APIView):
    def post(self, request):
        """회원가입"""
        serializer = UserSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response({"message": "회원가입이 완료되었습니다!"}, status=status.HTTP_201_CREATED)
        else:
            return Response({"message": f"{serializer.errors}"}, status=status.HTTP_400_BAD_REQUEST)

<로그인>

  • 로그인은 어렵지 않았다
  • simpleJWT 방식을 이용했는데 강의 때 배운 걸 써먹고 싶었다

- urls.py

# users/urls.py

from django.urls import path
from users import views
from rest_framework_simplejwt.views import (
    TokenRefreshView,
)


urlpatterns = [
    ...
    path('api/token/', views.CustomTokenObtainPairView.as_view(),
         name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

- serializers.py

# users/serializers.py

...
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
...

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
		
        # 토큰 페이로드 부분에 email, name, gender, age를 추가해보았다
        # Add custom claims
        token['email'] = user.email
        token['name'] = user.name
        token['gender'] = user.gender
        token['age'] = user.age

        return token

- views.py

# users/views.py

...
from users.serializers import CustomTokenObtainPairSerializer
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
)
...


class CustomTokenObtainPairView(TokenObtainPairView):
    """payload customizing"""
    serializer_class = CustomTokenObtainPairSerializer

- decoded JWT


<로그아웃>

- urls.py

# users/urls.py
...
from django.urls import path
from users import views
...


urlpatterns = [
    ...
    path('api/logout/', views.LogoutView.as_view(), name='logout_view'),
    ...
]

- views.py

# users/views.py
...
from rest_framework import status, permissions
from rest_framework.views import APIView
from rest_framework.response import Response
...


class LogoutView(APIView):
    permission_classes = [permissions.IsAuthenticated]

    def post(self, request):
        response = Response({
            "message": "로그아웃 되었습니다"
        }, status=status.HTTP_202_ACCEPTED)
        response.delete_cookie('refreshtoken')
        response.delete_cookie('accesstoken')
        return response
  • 포스트맨에서 실행했을 때 "로그아웃 되었습니다" 메시지가 나오긴 하는데 진짜로 로그아웃 된 건지는 모르겠다..
  • 나중에 할일목록 구현할 때 이게 진짜 되는지 아닌지 알 수 있을 것 같다...
  • 로그아웃이 되는 건지 안 되는 건지 잘 모르겠어서 다시 열심히 구글링을 해서 찾아봤다
  • 토큰은 직접 삭제하는 건 안 되고 만료될 때까지 기다려야 한다고 한다
  • 그래서 blacklist라는 기능을 사용해서 로그아웃 하려는 토큰을 blacklist에 넣어서 유효한 토큰이 아니게 만들어서 로그아웃이 되게 해보았다
  • 근데 이것도 사실 로그아웃이 된 건지...뭔지 잘 모르겠다 로그아웃 누르고 유저정보 수정(헤더에 로그아웃한 access 토큰이 있는 상태)을 하면 수정이 된다;;; 그럼 로그인 안 된 거 아닌가...?
  • 일단 로그아웃 기능 수정한 걸로 아래 기재하겠다

- settings.py

# 프로젝트/settings.py

SIMPLE_JWT = {
    ...
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': True,
    ...
}

- urls.py

# users/views.py

from django.urls import path
from rest_framework_simplejwt.views import (
    TokenBlacklistView,
)


urlpatterns = [
    ...
    path('api/logout/', TokenBlacklistView.as_view(), name='logout_view'),
    ...
]
  • DRF simple jwt 라이브러리에서 기본으로 제공하는 함수라서 따로 view를 작성하진 않아도 된다

<회원정보 수정>

  • 회원정보 수정에는 조건이 걸려있다
  • "아이디는 수정하게 두지 말 것"
  • 아이디가 pk말하는 게 아니라 아이디로 쓰고 있는 email 말하는 거 맞겠지?
  • 수정 함수는 강의 들었던 거 참고해서 금방 짰는데 저 조건을 어떻게 거는지가 문제였다
  • 회원정보를 수정하기 전 get방식의 회원정보 조회를 구현했는데 UserSerializer를 쓰기엔 모든 필드가 다 나와서 원하는 필드만 볼 수 있는 시리얼라이저를 새로 만들었다
  • 필드가 이메일(아이디), 이름, 성별, 나이, 소개, 마지막 로그인 시간으로 이루어져 있다
  • 근데 이 중에서 이름, 성별, 나이, 소개만 수정할 수 있게 하고 싶은데 이 시리얼라이저를 쓰면 이메일이랑 마지막로그인시간까지 수정이 되어버린다
  • 그래서 이름, 성별, 나이, 소개만 있는 시리얼라이저를 만들었는데, 그러고보니 수정하고 나서 원래의 회원정보 조회할 때 나오는 필드가 다 나왔으면 좋겠는데 이름, 성별, 나이, 소개만 나오는거다
  • 그래서 수정은 이름, 성별, 나이, 소개만 있는 시리얼라이저를 사용하고 리턴할 때는 원래 회원정보 조회할 때 쓰는 시리얼라이저 형태로 보이게끔 하려고 했는데.. 뭔가 코드가 추가되고  지저분해지는 것 같아서 이건 아니다 싶었다
  • 분명 방법이 있을 거 같은데...하다가 순간 머리에 read only라는 글자가 스쳐지나갔다...!
  • 바로 구글에 serializer field read only를 검색하니 사용방법을 바로 찾을 수 있었다(Write only, read only fields in django rest framework - Stack Overflow)

- urls.py

# users/urls.py
...
from django.urls import path
from users import views
...

urlpatterns = [
    ...
    path('api/<int:user_id>/', views.ProfileView.as_view(), name='profile_view'),
    ...
]

- serializers.py

# users/serializers.py
...
from rest_framework import serializers
from users.models import User
...


class UserInfoSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ("email", "name", "gender", "age",
                  "introduction", "last_login")
        extra_kwargs = {
            "email": {"read_only": True},
            "last_login": {"read_only": True}
        }

- views.py

# users/views.py
...
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.generics import get_object_or_404
from users.models import User
from users.serializers import UserInfoSerializer
...


class ProfileView(APIView):
    permission_classes = [permissions.IsAuthenticated]

    def get(self, request, user_id):
        """회원정보 보기"""
        user = get_object_or_404(User, id=user_id)
        serializer = UserInfoSerializer(user)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def put(self, request, user_id):
        """회원정보 수정"""
        user = get_object_or_404(User, id=user_id)
        if request.user == user:
            serializer = UserInfoSerializer(
                user, data=request.data, partial=True)
            if serializer.is_valid():
                serializer.save()
                return Response(serializer.data, status=status.HTTP_200_OK)
            else:
                return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        else:
            return Response({"message": "수정 권한이 없습니다!"}, status=status.HTTP_403_FORBIDDEN)

<회원탈퇴>

  • 회원탈퇴는 수정이랑 url은 같고 method만 다르다
  • 시리얼라이저는 사용하지 않는다
  • views.py만 작성하면 된다
# users/views.py
...
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.decorators import permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.generics import get_object_or_404
from users.models import User
...


class ProfileView(APIView):
    ...

    @permission_classes([IsAuthenticated])
    def delete(self, request, user_id):
        """회원탈퇴"""
        user = get_object_or_404(User, id=user_id)
        if request.user == user:
            request.user.delete()
            return Response({"message": "회원탈퇴가 완료되었습니다"}, status=status.HTTP_204_NO_CONTENT)
        else:
            return Response({"message": "권한이 없습니다!"}, status=status.HTTP_403_FORBIDDEN)