v0.0.31 | 비밀번호 변경 기능 추가
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:
2026-01-19 00:26:53 +09:00
parent d6bec2c883
commit 1c7f241b37
4 changed files with 86 additions and 4 deletions

View File

@ -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():

View File

@ -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"),

View File

@ -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(

View File

@ -1 +1 @@
v0.0.30
v0.0.31