본문 바로가기
DEV/Web 개발

Web개발 :: Code Review _ User 관리 기능 _TIL78

by 올커 2022. 12. 28.

■ JITHub 개발일지 78일차

□  TIL(Today I Learned) ::

Code Review _ User 관리 기능

 회원가입

def post(self, request):
        serializer = SignupSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()

            user = get_object_or_404(User, email=request.data["email"])

            secured_key = RefreshToken.for_user(user).access_token
            expired_at = datetime.fromtimestamp(secured_key['exp']).strftime("%Y-%m-%dT%H:%M:%S")

            ConfirmEmail.objects.create(secured_key=secured_key, expired_at=expired_at, user=user)

            frontend_site = "www.gaggamagga.shop" 
            absurl = f'https://{frontend_site}/confirm_email.html?secured_key={str(secured_key)}'
            email_body = '안녕하세요!' + user.username +"고객님 이메일인증을 하시려면 아래 사이트를 접속해주세요 \n" + absurl
            message = {'email_body': email_body,'to_email':user.email, 'email_subject':'이메일 인증' }
            Util.send_email(message)

            return Response({"message":"회원가입이 되었습니다."}, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class Util:
    @staticmethod
    def send_email(message):
        email = EmailMessage(subject=message['email_subject'], body=message['email_body'], to=[message['to_email']])
        EmailThread(email).start()
  1. SignupSerializer에서 데이터가 유효한지 확인 후 저장함
  • SignupSerializer
class SignupSerializer(serializers.ModelSerializer):
    repassword= serializers.CharField(error_messages={'required':'비밀번호를 입력해주세요.', 'blank':'비밀번호를 입력해주세요.', 'write_only':True})    
    term_check = serializers.BooleanField(error_messages={'required':'약관동의를 확인해주세요.', 'blank':'약관동의를 확인해주세요.', 'write_only':True})

    class Meta:
        model = User
        fields = ('username' ,'password', 'repassword', 'phone_number', 'email', 'term_check',)
        extra_kwargs = {
                        'username': {
                        'error_messages': {
                        'required': '아이디를 입력해주세요.',
                        'blank':'아이디를 입력해주세요',}},

                        'password':{'write_only':True,
                        'error_messages': {
                        'required':'비밀번호를 입력해주세요.',
                        'blank':'비밀번호를 입력해주세요.',}},

                        'email': {
                        'error_messages': {
                        'required': '이메일을 입력해주세요.',
                        'invalid': '알맞은 형식의 이메일을 입력해주세요.',
                        'blank':'이메일을 입력해주세요.',}},

                        'phone_number':{
                        'error_messages':{
                        'required': '휴대폰 번호를 입력해주세요.',}}}

    def validate(self, data):
        username = data.get('username')
        phone_number = data.get('phone_number')
        password = data.get('password')
        repassword = data.get('repassword')
        term_check = data.get('term_check')

        # 아이디 유효성 검사
        if username_validator(username):
            raise serializers.ValidationError(detail={"username":"아이디는 6자 이상 20자 이하의 숫자, 영문 대/소문자 이어야 합니다."})

        # 비밀번호 일치
        if password != repassword:
            raise serializers.ValidationError(detail={"password":"비밀번호가 일치하지 않습니다."})

        # 비밀번호 유효성 검사
        if password_validator(password):
            raise serializers.ValidationError(detail={"password":"비밀번호는 8자 이상 16자이하의 영문 대/소문자, 숫자, 특수문자 조합이어야 합니다. "})

        # 비밀번호 동일여부 검사
        if password_pattern(password):
            raise serializers.ValidationError(detail={"password":"비밀번호는 3자리 이상 동일한 영문,숫자,특수문자 사용 불가합니다. "})

        # 휴대폰 번호 존재여부와 blank 허용
        if User.objects.filter(phone_number=phone_number).exists() and not phone_number=='':
            raise serializers.ValidationError(detail={"phone_number":"이미 사용중인 휴대폰 번호 이거나 탈퇴한 휴대폰 번호입니다."})

        # 이용약관 확인 검사
        if term_check == False:
            raise serializers.ValidationError(detail={"term_check":"약관동의를 확인해주세요."})

        return data

    def create(self, validated_data):
        username = validated_data['username']
        email = validated_data['email']
        phone_number = validated_data['phone_number']

        user= User(
            username=username,
            phone_number=phone_number,
            email=email, 
        )
        user.set_password(validated_data['password'])
        user.save()

        Profile.objects.create(user=user) 
        return user
  • term_check와 repassword을 휘발성으로 사용하기 위해서 field를 만들어줬다.
  • 해당하는 validation은 전부 validator.py에서 import하여 사용하고 있다.

validator.py

import re

def password_validator(password):
    password_validation = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,16}"

    if not re.search(password_validation, str(password)):
        return True
    return False
        
def password_pattern(password):
    password_pattern = r"(.)\1+\1"

    if re.search(password_pattern, str(password)):
        return True
    return False

def username_validator(username):
    username_validations = r'^[A-Za-z0-9]{6,20}$'

    if not re.search(username_validations, str(username)):
        return True
    return False
    
def nickname_validator(nickname):
    nickname_validation = r'^[A-Za-z가-힣0-9]{3,10}$'

    if not re.search(nickname_validation, str(nickname)):
        return True
    return False
  1. User의 이메일을 찾아 유저의 RefreshToken(simplejwt)로 secured_ket를 생성한다. 생성하는 이유는? secured_key로 인증하기 위해서이다. expired_at은 토큰 값으 만료시간을 나타내기 위해서이다. (datetime.fromtimestamp 관련)
  2. ConfrimEmail 모델에 저장한다.
  3. 이메을 보내줄 내용과 제목과 보낼 사람을 적는다.
  4. utils.py에 작성한 이메일 발송 함수에 맞게 값을 넣어 보낸다.

 


 로그아웃

def post(self, request):
    serializer = LogoutSerializer(data=request.data)
    if serializer.is_valid():
        serializer.save()
        return Response({"message":"로그아웃 성공되었습니다."}, status=status.HTTP_200_OK)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class LogoutSerializer(serializers.Serializer):
    refresh = serializers.CharField()
    
    def validate(self, attrs):
        self.token = attrs['refresh']
        return attrs

    def save(self, **kwargs):
        try:
            RefreshToken(self.token).blacklist()

        except TokenError:
            raise serializers.ValidationError(detail={"만료된 토큰":"유효하지 않거나 만료된 토큰입니다."})
  1. RefreshToken을 받아 Logoutserializer로 보내어 blacklist()라는 곳에 저장한다. (blacklist 관련 링크) blacklist라는 것을 사용하게 되면 재사용되는 토큰을 막을 수 있고 토큰의 기록을 볼 수 있다.
  2. 리눅스의 crontab 명령어를 사용하여 python manage.py flushexpiredtokens를 주기적으로 갱신하면 blacklist에 저장되어있던 것들을 초기화 할 수 있다.

회원정보 수정

def put(self, request):
    user = get_object_or_404(User, id=request.user.id)
    serializer = UserUpdateSerializer(user, data=request.data, context={'request': request})
    if serializer.is_valid():
        serializer.save()
        return Response({"message":"회원 수정이 완료되었습니다."}, status=status.HTTP_200_OK)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

 

회원 비활성화

def delete(self, request):
        user = get_object_or_404(User, id=request.user.id)
        user.withdraw = True
        user.withdraw_at = timezone.now()
        user.save()
        return Response({"message":"회원 비활성화가 되었습니다."}, status=status.HTTP_200_OK)
  1. 회원을 삭제하는 것이 아닌 비활성화를 하고 비활성화 시간을 저장하도록 했다.
  2. 회원에 대한 정보들을 바로 삭제가 아닌 별도 보관을 하기 위해서 비활성화를 선택했다.
class UserStatusChange:

    def __init__(self):
        self.year_ago = timezone.now() - timezone.timedelta(days=365)
        self.two_months_ago = timezone.now() - timezone.timedelta(days=60)

    # 회원정보 보유기간 지나면 withdraw True로 만듬
    def user_withdraw_send_email(self):
        user = User.objects.filter(is_admin=False, last_login__lte=self.year_ago)

        inactivate_user_email = user.values("email")
        inactivate_email_subject = '가까? 마까? 휴면회원 계정 처리 안내'
        inactivate_email_body = '고객님 안녕하세요. 가까? 마까?입니다. 저희는 소중한 고객님의 개인정보 보호를 위해 관계 법령에 따라 고객님의 온라인 계정을 별도로 분리 보관할 예정입니다. 휴면계정을 해제하기 위해서는 별도의 인증 동의 절차가 진행될 수 있으므로 편리한 계정 사용이 필요하시다면 지금 바로 가까? 마까?를 방문해주세요.'

        if user:
            for i in inactivate_user_email:
                message = {'email_body': inactivate_email_body, 'to_email': i["email"],'email_subject': inactivate_email_subject}
                Util.send_email(message)
            user.update(withdraw=True)

    # 비활성화가 되고 60일이 지나면 삭제
    def user_withdraw_delete(self):
        user = User.objects.filter(is_admin=False,  withdraw_at__lte=self.two_months_ago, withdraw=True)
        
        delete_user_email = user.values("email")
        delete_email_subject = '가까? 마까? 비활성화 계정 삭제'
        delete_email_body = '고객님 안녕하세요. 가까? 마까?입니다. 저희는 비활성화계정을 삭제합니다.'

        if user:
            for i in delete_user_email:
                message = {'email_body': delete_email_body, 'to_email': i["email"],'email_subject': delete_email_subject}
                Util.send_email(message)
            user.delete()

    # 로그인 비밀번호 변경이 60일이 지났을 경우 password_expired를 True로 바꿈
    def user_password_expired(self):
        user = User.objects.filter(is_admin=False, last_password_changed__lte=self.two_months_ago)
        user.update(password_expired=True)
  1. 매일 서버가 12시 정각이 되면 crontab이라는 명령어로 특정 명령어를 실행할 수 있도록 하는데 위의 코드를 실행시킨다.(crontab관련 링크)
  2. 회원정보 보유기간은 1년으로 설정이 되어있으며 1년이 넘어가게 되면 이메일을 발송하고 user의 상태를 withdraw로 True로 전환시킨다.
  3. 비활성화가되고 60일이 지나면 이메일을 보내고 유저의 정보를 삭제한다.
  4. 비밀번호 변경일 같은 경우 비밀번호 만료기능이 있는데 만료일이 60로 기준으로 60일이 지났을 경우 password_expired를 True로 바꾼다.

공개프로필

# models.py

# 프로필
class Profile(models.Model):
    profile_image = models.ImageField('프로필 사진', default='default_profile_pic.jpg', upload_to='profile_pics', validators=[validate_image_file_extension])
    nickname = models.CharField('닉네임', max_length=10, null=True, unique=True, error_messages={"unique": "이미 사용중인 닉네임 이거나 탈퇴한 닉네임입니다."})
    intro = models.CharField('자기소개', max_length=100, null=True)
    review_cnt = models.PositiveIntegerField('리뷰수', default=0)

    user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="회원", related_name='user_profile')
    
    followings = models.ManyToManyField('self', symmetrical=False, blank=True, related_name= 'followers')
# serializers.py

# 공개 프로필 serializer
class PublicProfileSerializer(serializers.ModelSerializer):
    followings = PrivateProfileSerializer(many=True)
    followers = PrivateProfileSerializer(many=True)
    review_set = ReviewListSerializer(many=True, source='user.review_set')
    bookmark_place = PlaceSerializer(many=True, source='user.bookmark_place')
    user_id = serializers.SerializerMethodField()

    def get_user_id(self, obj):
        return obj.user.id

    class Meta:
        model = Profile
        fields = ('id', 'user_id', 'nickname', 'profile_image', 'intro', 'followings', 'followers', 'review_set', 'bookmark_place',)
  • bookmark_place = PlaceSerializer(many=True, source='user.bookmark_place')
  • profile과 OneToOne으로 연결되어있는 user의 북마크, 리뷰에 접근하기 위해 source를 사용

 

반응형

댓글