v0.0.31 | 비밀번호 변경 기능 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 3m38s
All checks were successful
Build And Test / build-and-push (push) Successful in 3m38s
- ChangePasswordView API 추가 (사용자 본인 비밀번호 변경) - 소셜 로그인 계정 비밀번호 설정 지원 - 관리자 비밀번호 초기화 기능 (UserUpdateView) - RegisterSerializer에 has_password 필드 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -5,13 +5,18 @@ from rest_framework.exceptions import ValidationError
|
|||||||
|
|
||||||
class RegisterSerializer(serializers.ModelSerializer):
|
class RegisterSerializer(serializers.ModelSerializer):
|
||||||
password = serializers.CharField(write_only=True, required=False)
|
password = serializers.CharField(write_only=True, required=False)
|
||||||
|
has_password = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
fields = ("email", "name", "password", "grade", "desc",
|
fields = ("email", "name", "password", "grade", "desc",
|
||||||
"phone", "address", "gender", "birth_date", "education",
|
"phone", "address", "gender", "birth_date", "education",
|
||||||
"social_provider", "profile_image")
|
"social_provider", "profile_image", "has_password")
|
||||||
read_only_fields = ("social_provider", "profile_image")
|
read_only_fields = ("social_provider", "profile_image", "has_password")
|
||||||
|
|
||||||
|
def get_has_password(self, obj):
|
||||||
|
"""사용자가 비밀번호를 가지고 있는지 여부"""
|
||||||
|
return obj.has_usable_password()
|
||||||
|
|
||||||
def validate_email(self, value):
|
def validate_email(self, value):
|
||||||
if CustomUser.objects.filter(email=value).exists():
|
if CustomUser.objects.filter(email=value).exists():
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import (
|
from .views import (
|
||||||
RegisterView, MeView, CustomTokenObtainPairView,
|
RegisterView, MeView, ChangePasswordView, CustomTokenObtainPairView,
|
||||||
SSHKeyUploadView, SSHKeyInfoView, SSHKeyRetrieveView,
|
SSHKeyUploadView, SSHKeyInfoView, SSHKeyRetrieveView,
|
||||||
UserListView, UserUpdateView,
|
UserListView, UserUpdateView,
|
||||||
NHNCloudCredentialsView, NHNCloudPasswordView,
|
NHNCloudCredentialsView, NHNCloudPasswordView,
|
||||||
@ -25,6 +25,7 @@ urlpatterns = [
|
|||||||
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
path('verify/', TokenVerifyView.as_view(), name='token_verify'),
|
path('verify/', TokenVerifyView.as_view(), name='token_verify'),
|
||||||
path('me/', MeView.as_view(), name='me'),
|
path('me/', MeView.as_view(), name='me'),
|
||||||
|
path('me/password/', ChangePasswordView.as_view(), name='change_password'),
|
||||||
path("ssh-key/", SSHKeyUploadView.as_view(), name="ssh_key_upload"),
|
path("ssh-key/", SSHKeyUploadView.as_view(), name="ssh_key_upload"),
|
||||||
path("ssh-key/info/", SSHKeyInfoView.as_view(), name="ssh_key_info"),
|
path("ssh-key/info/", SSHKeyInfoView.as_view(), name="ssh_key_info"),
|
||||||
path("ssh-key/view/", SSHKeyRetrieveView.as_view(), name="ssh_key_retrieve"),
|
path("ssh-key/view/", SSHKeyRetrieveView.as_view(), name="ssh_key_retrieve"),
|
||||||
|
|||||||
@ -81,6 +81,67 @@ class MeView(APIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordView(APIView):
|
||||||
|
"""사용자 본인 비밀번호 변경"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
with tracer.start_as_current_span("ChangePasswordView POST") as span:
|
||||||
|
email, ip, ua = get_request_info(request)
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
current_password = request.data.get("current_password")
|
||||||
|
new_password = request.data.get("new_password")
|
||||||
|
confirm_password = request.data.get("confirm_password")
|
||||||
|
|
||||||
|
# 필수 필드 확인
|
||||||
|
if not current_password or not new_password or not confirm_password:
|
||||||
|
return Response(
|
||||||
|
{"error": "현재 비밀번호, 새 비밀번호, 비밀번호 확인은 필수입니다."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# 새 비밀번호 확인
|
||||||
|
if new_password != confirm_password:
|
||||||
|
return Response(
|
||||||
|
{"error": "새 비밀번호가 일치하지 않습니다."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# 비밀번호 길이 확인
|
||||||
|
if len(new_password) < 8:
|
||||||
|
return Response(
|
||||||
|
{"error": "비밀번호는 최소 8자 이상이어야 합니다."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# 소셜 로그인 전용 계정인 경우 (비밀번호 없음)
|
||||||
|
if not user.has_usable_password():
|
||||||
|
# 현재 비밀번호 없이 새 비밀번호 설정 가능
|
||||||
|
user.set_password(new_password)
|
||||||
|
user.save()
|
||||||
|
logger.info(f"[PASSWORD SET] user={email} | status=success | IP={ip} | UA={ua}")
|
||||||
|
span.add_event("Password set for social user", attributes={"email": email})
|
||||||
|
return Response({"message": "비밀번호가 설정되었습니다."})
|
||||||
|
|
||||||
|
# 현재 비밀번호 확인
|
||||||
|
if not user.check_password(current_password):
|
||||||
|
logger.warning(f"[PASSWORD CHANGE] user={email} | status=fail | reason=wrong_password | IP={ip} | UA={ua}")
|
||||||
|
return Response(
|
||||||
|
{"error": "현재 비밀번호가 일치하지 않습니다."},
|
||||||
|
status=status.HTTP_401_UNAUTHORIZED
|
||||||
|
)
|
||||||
|
|
||||||
|
# 새 비밀번호 설정
|
||||||
|
user.set_password(new_password)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
logger.info(f"[PASSWORD CHANGE] user={email} | status=success | IP={ip} | UA={ua}")
|
||||||
|
span.add_event("Password changed", attributes={"email": email})
|
||||||
|
|
||||||
|
return Response({"message": "비밀번호가 변경되었습니다."})
|
||||||
|
|
||||||
|
|
||||||
class CustomTokenObtainPairView(TokenObtainPairView):
|
class CustomTokenObtainPairView(TokenObtainPairView):
|
||||||
serializer_class = CustomTokenObtainPairSerializer
|
serializer_class = CustomTokenObtainPairSerializer
|
||||||
|
|
||||||
@ -314,6 +375,21 @@ class UserUpdateView(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
update_fields.append('grade')
|
update_fields.append('grade')
|
||||||
actions.append(f"grade_changed_to_{grade}")
|
actions.append(f"grade_changed_to_{grade}")
|
||||||
|
|
||||||
|
# 비밀번호 초기화 (관리자 전용)
|
||||||
|
new_password = request.data.get('new_password')
|
||||||
|
if new_password is not None:
|
||||||
|
if len(new_password) < 8:
|
||||||
|
return Response(
|
||||||
|
{"error": "비밀번호는 최소 8자 이상이어야 합니다."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
instance.set_password(new_password)
|
||||||
|
instance.save() # set_password 후 별도 save 필요
|
||||||
|
actions.append("password_reset")
|
||||||
|
logger.info(
|
||||||
|
f"[USER PASSWORD RESET] admin={admin_email} | target={target_email} | IP={ip} | UA={ua}"
|
||||||
|
)
|
||||||
|
|
||||||
if update_fields:
|
if update_fields:
|
||||||
instance.save(update_fields=update_fields)
|
instance.save(update_fields=update_fields)
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
Reference in New Issue
Block a user