Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions core/auth/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from rest_framework.exceptions import AuthenticationFailed
from drf_spectacular.extensions import OpenApiAuthenticationExtension

from core.utils.cookie import ACCESS_TOKEN_COOKIE


class CustomJWTAuthentication(JWTAuthentication):
def authenticate(self, request):
raw_token = request.COOKIES.get("access_token")
raw_token = request.COOKIES.get(ACCESS_TOKEN_COOKIE)

if raw_token is None:
return None
Expand All @@ -26,5 +28,5 @@ def get_security_definition(self, auto_schema):
return {
"type": "apiKey",
"in": "cookie",
"name": "access_token",
"name": ACCESS_TOKEN_COOKIE,
}
31 changes: 17 additions & 14 deletions core/auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
set_secure_cookie,
ACCESS_TOKEN_LIFETIME,
REFRESH_TOKEN_LIFETIME,
ACCESS_TOKEN_COOKIE,
REFRESH_TOKEN_COOKIE,
IS_REGISTERED_COOKIE,
)
from core.auth.services import SocialLoginService
from .serializers import (
Expand Down Expand Up @@ -106,19 +109,19 @@ def post(self, request):

set_secure_cookie(
response,
"access_token",
ACCESS_TOKEN_COOKIE,
access_token,
max_age=int(ACCESS_TOKEN_LIFETIME.total_seconds()),
)
set_secure_cookie(
response,
"refresh_token",
REFRESH_TOKEN_COOKIE,
refresh_token,
max_age=int(REFRESH_TOKEN_LIFETIME.total_seconds()),
)
set_secure_cookie(
response,
"is_registered",
IS_REGISTERED_COOKIE,
str(is_registered).lower(),
max_age=int(REFRESH_TOKEN_LIFETIME.total_seconds()),
)
Expand All @@ -142,7 +145,7 @@ class LogoutView(APIView):
},
)
def post(self, request):
refresh_token = request.COOKIES.get("refresh_token")
refresh_token = request.COOKIES.get(REFRESH_TOKEN_COOKIE)

if refresh_token:
try:
Expand All @@ -159,9 +162,9 @@ def post(self, request):

response = Response(status=status.HTTP_204_NO_CONTENT)

response.delete_cookie("access_token")
response.delete_cookie("refresh_token")
response.delete_cookie("is_registered")
response.delete_cookie(ACCESS_TOKEN_COOKIE)
response.delete_cookie(REFRESH_TOKEN_COOKIE)
response.delete_cookie(IS_REGISTERED_COOKIE)

return response

Expand All @@ -181,7 +184,7 @@ class TokenRefreshView(APIView):
},
)
def post(self, request):
refresh_token = request.COOKIES.get("refresh_token")
refresh_token = request.COOKIES.get(REFRESH_TOKEN_COOKIE)

if not refresh_token:
return Response(
Expand Down Expand Up @@ -226,19 +229,19 @@ def post(self, request):

set_secure_cookie(
response,
"access_token",
ACCESS_TOKEN_COOKIE,
new_access_token,
max_age=int(ACCESS_TOKEN_LIFETIME.total_seconds()),
)
set_secure_cookie(
response,
"refresh_token",
REFRESH_TOKEN_COOKIE,
new_refresh_token,
max_age=int(REFRESH_TOKEN_LIFETIME.total_seconds()),
)
set_secure_cookie(
response,
"is_registered",
IS_REGISTERED_COOKIE,
str(is_registered).lower(),
max_age=int(REFRESH_TOKEN_LIFETIME.total_seconds()),
)
Expand Down Expand Up @@ -365,19 +368,19 @@ def post(self, request):
# 테스트용 access token 쿠키는 토큰 만료 시간과 동일하게 설정
set_secure_cookie(
response,
"access_token",
ACCESS_TOKEN_COOKIE,
access_token,
max_age=int(test_token_lifetime.total_seconds()),
)
set_secure_cookie(
response,
"refresh_token",
REFRESH_TOKEN_COOKIE,
refresh_token,
max_age=int(REFRESH_TOKEN_LIFETIME.total_seconds()),
)
set_secure_cookie(
response,
"is_registered",
IS_REGISTERED_COOKIE,
str(is_registered).lower(),
max_age=int(REFRESH_TOKEN_LIFETIME.total_seconds()),
)
Expand Down
36 changes: 36 additions & 0 deletions core/user/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from core.models import Study


class UserService:
"""사용자 관련 비즈니스 로직 서비스"""

class StudyOwnerCannotDeleteError(Exception):
"""스터디장인 경우 탈퇴 불가 예외"""

def __init__(self, study_names: list[str]):
self.study_names = study_names
super().__init__("스터디장인 스터디가 있어 탈퇴할 수 없습니다.")

def delete_user(self, user):
"""
사용자를 삭제합니다.

스터디장인 경우 먼저 스터디장 권한을 위임해야 합니다.

Args:
user: 삭제할 사용자 인스턴스

Returns:
None

Raises:
StudyOwnerCannotDeleteError: 스터디장인 경우
"""
# 스터디 오너인 경우 탈퇴 불가
owned_studies = Study.objects.filter(owner=user)
if owned_studies.exists():
study_names = list(owned_studies.values_list("name", flat=True))
raise self.StudyOwnerCannotDeleteError(study_names)

# 사용자 삭제
user.delete()
43 changes: 39 additions & 4 deletions core/user/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@

from core.utils.cookie import (
set_secure_cookie,
delete_auth_cookies,
ACCESS_TOKEN_LIFETIME,
REFRESH_TOKEN_LIFETIME,
ACCESS_TOKEN_COOKIE,
REFRESH_TOKEN_COOKIE,
IS_REGISTERED_COOKIE,
)
from .serializers import (
UserInfoSerializer,
Expand All @@ -17,12 +21,13 @@
UsernameValidationSerializer,
BojUsernameValidationSerializer,
)
from .services import UserService
from core.common.serializers import ErrorEnvelopeSerializer


@extend_schema(tags=["user"])
class UserMeView(APIView):
"""현재 로그인한 사용자 정보 조회 API"""
"""현재 로그인한 사용자 정보 조회 및 탈퇴 API"""

permission_classes = [IsAuthenticated]

Expand All @@ -35,6 +40,36 @@ def get(self, request):
serializer = UserInfoSerializer(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)

@extend_schema(
summary="회원 탈퇴",
description="현재 로그인한 사용자의 계정을 삭제합니다. 스터디장인 경우 먼저 스터디장 권한을 위임해야 합니다.",
responses={
204: None,
400: ErrorEnvelopeSerializer,
},
)
def delete(self, request):
user = request.user
user_service = UserService()

try:
user_service.delete_user(user)
except UserService.StudyOwnerCannotDeleteError as e:
study_list = ", ".join(e.study_names)
return Response(
{
"error_code": "STUDY_OWNER_CANNOT_DELETE",
"message": f"스터디장인 스터디({study_list})가 있어 탈퇴할 수 없습니다. 먼저 스터디장을 위임해주세요.",
},
status=status.HTTP_400_BAD_REQUEST,
)

# 로그아웃 처리 (쿠키 삭제)
response = Response(status=status.HTTP_204_NO_CONTENT)
delete_auth_cookies(response)

return response


@extend_schema(tags=["user"])
class UserProfileView(APIView):
Expand Down Expand Up @@ -75,19 +110,19 @@ def patch(self, request):

set_secure_cookie(
response,
"access_token",
ACCESS_TOKEN_COOKIE,
access_token,
max_age=ACCESS_TOKEN_LIFETIME,
)
set_secure_cookie(
response,
"refresh_token",
REFRESH_TOKEN_COOKIE,
refresh_token,
max_age=REFRESH_TOKEN_LIFETIME,
)
set_secure_cookie(
response,
"is_registered",
IS_REGISTERED_COOKIE,
"true",
max_age=REFRESH_TOKEN_LIFETIME,
)
Expand Down
17 changes: 17 additions & 0 deletions core/utils/cookie.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
ACCESS_TOKEN_LIFETIME = settings.SIMPLE_JWT["ACCESS_TOKEN_LIFETIME"]
REFRESH_TOKEN_LIFETIME = settings.SIMPLE_JWT["REFRESH_TOKEN_LIFETIME"]

# Cookie names
ACCESS_TOKEN_COOKIE = "access_token"
REFRESH_TOKEN_COOKIE = "refresh_token"
IS_REGISTERED_COOKIE = "is_registered"


def _get_secure_cookie_kwargs(max_age):
"""
Expand Down Expand Up @@ -35,3 +40,15 @@ def set_secure_cookie(response, key, value, max_age):
max_age: 쿠키 만료 시간 (초 단위)
"""
response.set_cookie(key, value, **_get_secure_cookie_kwargs(max_age))


def delete_auth_cookies(response):
"""
인증 관련 쿠키를 삭제하는 헬퍼 함수

Args:
response: Django Response 객체
"""
response.delete_cookie(ACCESS_TOKEN_COOKIE)
response.delete_cookie(REFRESH_TOKEN_COOKIE)
response.delete_cookie(IS_REGISTERED_COOKIE)