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):
|
||||
password = serializers.CharField(write_only=True, required=False)
|
||||
has_password = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ("email", "name", "password", "grade", "desc",
|
||||
"phone", "address", "gender", "birth_date", "education",
|
||||
"social_provider", "profile_image")
|
||||
read_only_fields = ("social_provider", "profile_image")
|
||||
"social_provider", "profile_image", "has_password")
|
||||
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):
|
||||
if CustomUser.objects.filter(email=value).exists():
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
RegisterView, MeView, CustomTokenObtainPairView,
|
||||
RegisterView, MeView, ChangePasswordView, CustomTokenObtainPairView,
|
||||
SSHKeyUploadView, SSHKeyInfoView, SSHKeyRetrieveView,
|
||||
UserListView, UserUpdateView,
|
||||
NHNCloudCredentialsView, NHNCloudPasswordView,
|
||||
@ -25,6 +25,7 @@ urlpatterns = [
|
||||
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path('verify/', TokenVerifyView.as_view(), name='token_verify'),
|
||||
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/info/", SSHKeyInfoView.as_view(), name="ssh_key_info"),
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
serializer_class = CustomTokenObtainPairSerializer
|
||||
|
||||
@ -314,6 +375,21 @@ class UserUpdateView(generics.RetrieveUpdateDestroyAPIView):
|
||||
update_fields.append('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:
|
||||
instance.save(update_fields=update_fields)
|
||||
logger.info(
|
||||
|
||||
Reference in New Issue
Block a user