■ 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()
- 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
- 파이썬 정규표현식을 사용했다.
- re.search와 re.match가 있는데 링크 참고
- create 부분에서 user를 생성할 때 프로필도 생성되도록 하였다.
- User의 이메일을 찾아 유저의 RefreshToken(simplejwt)로 secured_ket를 생성한다. 생성하는 이유는? secured_key로 인증하기 위해서이다. expired_at은 토큰 값으 만료시간을 나타내기 위해서이다. (datetime.fromtimestamp 관련)
- ConfrimEmail 모델에 저장한다.
- 이메을 보내줄 내용과 제목과 보낼 사람을 적는다.
- 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={"만료된 토큰":"유효하지 않거나 만료된 토큰입니다."})
- RefreshToken을 받아 Logoutserializer로 보내어 blacklist()라는 곳에 저장한다. (blacklist 관련 링크) blacklist라는 것을 사용하게 되면 재사용되는 토큰을 막을 수 있고 토큰의 기록을 볼 수 있다.
- 리눅스의 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)
- 회원을 삭제하는 것이 아닌 비활성화를 하고 비활성화 시간을 저장하도록 했다.
- 회원에 대한 정보들을 바로 삭제가 아닌 별도 보관을 하기 위해서 비활성화를 선택했다.
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)
- 매일 서버가 12시 정각이 되면 crontab이라는 명령어로 특정 명령어를 실행할 수 있도록 하는데 위의 코드를 실행시킨다.(crontab관련 링크)
- 회원정보 보유기간은 1년으로 설정이 되어있으며 1년이 넘어가게 되면 이메일을 발송하고 user의 상태를 withdraw로 True로 전환시킨다.
- 비활성화가되고 60일이 지나면 이메일을 보내고 유저의 정보를 삭제한다.
- 비밀번호 변경일 같은 경우 비밀번호 만료기능이 있는데 만료일이 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를 사용
반응형
'DEV > Web 개발' 카테고리의 다른 글
Web개발 :: Code Review _ Review CRUD 기능 _TIL80 (1) | 2022.12.28 |
---|---|
Web개발 :: Code Review _ Place 추천 기능 _TIL79 (0) | 2022.12.28 |
Web 개발 :: 12월 넷째주 WIL17 (0) | 2022.12.28 |
Web개발 :: github action을 활용한 CI/CD _TIL77 (0) | 2022.12.28 |
Web개발 :: Pagination, Crawling, Localstorage _TIL76 (1) | 2022.12.20 |
댓글