From ee61d155620a820f51230c29b326cac5eea6202b Mon Sep 17 00:00:00 2001 From: Moon-Nights Date: Fri, 26 Jul 2024 18:45:53 +0900 Subject: [PATCH 1/3] naengttogi-django-common-login-task --- Django/app/settings.py | 31 +++++++++ Django/app/urls.py | 1 + Django/common_user/__init__.py | 0 Django/common_user/admin.py | 3 + Django/common_user/apps.py | 6 ++ Django/common_user/migrations/__init__.py | 0 Django/common_user/models.py | 58 +++++++++++++++++ Django/common_user/serializers.py | 37 +++++++++++ Django/common_user/tests.py | 3 + Django/common_user/urls.py | 8 +++ Django/common_user/utils.py | 10 +++ Django/common_user/views.py | 78 +++++++++++++++++++++++ 12 files changed, 235 insertions(+) create mode 100644 Django/common_user/__init__.py create mode 100644 Django/common_user/admin.py create mode 100644 Django/common_user/apps.py create mode 100644 Django/common_user/migrations/__init__.py create mode 100644 Django/common_user/models.py create mode 100644 Django/common_user/serializers.py create mode 100644 Django/common_user/tests.py create mode 100644 Django/common_user/urls.py create mode 100644 Django/common_user/utils.py create mode 100644 Django/common_user/views.py diff --git a/Django/app/settings.py b/Django/app/settings.py index c546dd6..d5eff02 100644 --- a/Django/app/settings.py +++ b/Django/app/settings.py @@ -64,6 +64,8 @@ "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.google", # Google provider 추가 + # # Common Login + "common_user", ] @@ -208,3 +210,32 @@ }, }, } + + + +# # Common Login +# AUTH_USER_MODEL = "common_user.commonUser" # Added Custom User + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), +} + +from datetime import timedelta + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), +} + +# 이메일 설정 +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.gmail.com' # Gmail SMTP 서버 사용 +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = 'your-email@gmail.com' # 실제 이메일 주소 +EMAIL_HOST_PASSWORD = 'your-email-password' # 실제 비밀번호 또는 앱 비밀번호 + + + diff --git a/Django/app/urls.py b/Django/app/urls.py index a8227f7..231bfe4 100644 --- a/Django/app/urls.py +++ b/Django/app/urls.py @@ -47,4 +47,5 @@ ), name="schema-redoc", ), + path('api/common_user/', include('common_users.urls')), # added common_user ] diff --git a/Django/common_user/__init__.py b/Django/common_user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Django/common_user/admin.py b/Django/common_user/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Django/common_user/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Django/common_user/apps.py b/Django/common_user/apps.py new file mode 100644 index 0000000..124a32a --- /dev/null +++ b/Django/common_user/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommonUserConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'common_user' diff --git a/Django/common_user/migrations/__init__.py b/Django/common_user/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Django/common_user/models.py b/Django/common_user/models.py new file mode 100644 index 0000000..6900fae --- /dev/null +++ b/Django/common_user/models.py @@ -0,0 +1,58 @@ +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils import timezone + + +# Create your models here. + +import secrets + +class Common_user(AbstractUser): + + def create_user(self, email, username, nickname, password=None): + if not email: + raise ValueError(_('The Email field must be set')) + email = self.normalize_email(email) + user = self.model(email=email, username=username, nickname=nickname) + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, username, nickname, password): + user = self.create_user(email, username, nickname, password) + user.is_staff = True + user.is_superuser = True + user.save(using=self._db) + return user + + + class Common_user(AbstractBaseUser, PermissionsMixin): + email = models.EmailField(_('email address'), unique=True) + username = models.CharField(_('username'), max_length=30, unique=True) + nickname = models.CharField(_('nickname'), max_length=30) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + date_joined = models.DateTimeField(auto_now_add=True) + + objects = Common_userManager() + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email', 'nickname'] + + def __str__(self): + return self.email + +# Create Token + class EmailVerificationToken(models.Model): + user = models.OneToOneField(CustomUser, on_delete=models.CASCADE) + token = models.CharField(max_length=64, unique=True) + created_at = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + if not self.token: + self.token = secrets.token_urlsafe(32) + super().save(*args, **kwargs) + + def is_valid(self): + return (timezone.now() - self.created_at).days < 1 diff --git a/Django/common_user/serializers.py b/Django/common_user/serializers.py new file mode 100644 index 0000000..8aa1c10 --- /dev/null +++ b/Django/common_user/serializers.py @@ -0,0 +1,37 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +from django.contrib.auth.password_validation import validate_password + +User = get_user_model() + +class UserRegistrationSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) + password2 = serializers.CharField(write_only=True, required=True) + + class Meta: + model = User + fields = ('email', 'username', 'nickname', 'password', 'password2') + + def validate(self, attrs): + if attrs['password'] != attrs['password2']: + raise serializers.ValidationError({"password": "Password fields didn't match."}) + return attrs + + def create(self, validated_data): + user = User.objects.create_user( + email=validated_data['email'], + username=validated_data['username'], + nickname=validated_data['nickname'], + password=validated_data['password'] + ) + return user + +class UserLoginSerializer(serializers.Serializer): + username_or_email = serializers.CharField(required=True) + password = serializers.CharField(required=True) + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('id', 'email', 'username', 'nickname') \ No newline at end of file diff --git a/Django/common_user/tests.py b/Django/common_user/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Django/common_user/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Django/common_user/urls.py b/Django/common_user/urls.py new file mode 100644 index 0000000..78323f5 --- /dev/null +++ b/Django/common_user/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from .views import UserRegistrationView, UserLoginView, VerifyEmailView + +urlpatterns = [ + path('register/', UserRegistrationView.as_view(), name='register'), + path('login/', UserLoginView.as_view(), name='login'), + path('verify-email//', VerifyEmailView.as_view(), name='verify_email'), +] \ No newline at end of file diff --git a/Django/common_user/utils.py b/Django/common_user/utils.py new file mode 100644 index 0000000..a4ace47 --- /dev/null +++ b/Django/common_user/utils.py @@ -0,0 +1,10 @@ +from django.core.mail import send_mail +from django.conf import settings + +# 이메일 전송 함수 +def send_verification_email(user, token): + subject = '이메일 주소 확인' + message = f'다음 링크를 클릭하여 이메일 주소를 확인해주세요: http://yourdomain.com/verify-email/{token}' + email_from = settings.EMAIL_HOST_USER + recipient_list = [user.email,] + send_mail(subject, message, email_from, recipient_list) \ No newline at end of file diff --git a/Django/common_user/views.py b/Django/common_user/views.py new file mode 100644 index 0000000..3243b33 --- /dev/null +++ b/Django/common_user/views.py @@ -0,0 +1,78 @@ +from django.shortcuts import render +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework_simplejwt.tokens import RefreshToken +from django.contrib.auth import authenticate, get_user_model +from .serializers import UserRegistrationSerializer, UserLoginSerializer, UserSerializer +from .models import EmailVerificationToken +from .utils import send_verification_email + +# Create your views here. + +User = get_user_model() + +class UserRegistrationView(APIView): + def post(self, request): + serializer = UserRegistrationSerializer(data=request.data) + if serializer.is_valid(): + user = serializer.save() + user.is_active = False # 이메일 인증 전까지 계정을 비활성화 처리 + user.save() + refresh = RefreshToken.for_user(user) + token = EmailVerificationToken.objects.create(user=user) # 이메일 인증 토큰 생성 + send_verification_email(user, token.token) # 인증 이메일 발송 + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + 'message': '회원가입이 완료되었습니다. 이메일을 확인하여 계정을 활성화해주세요.', + 'user': UserSerializer(user).data + }, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class UserLoginView(APIView): + def post(self, request): + serializer = UserLoginSerializer(data=request.data) + if serializer.is_valid(): + username_or_email = serializer.validated_data['username_or_email'] + password = serializer.validated_data['password'] + + # 이메일 또는 아이디로 로그인 시도 + user = None + if '@' in username_or_email: + try: + user = User.objects.get(email=username_or_email) + except User.DoesNotExist: + pass + + if not user: + user = authenticate(username=username_or_email, password=password) + else: + if not user.check_password(password): + user = None + + if user: + refresh = RefreshToken.for_user(user) + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + 'user': UserSerializer(user).data + }) + return Response({'error': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + class VerifyEmailView(APIView): + def get(self, request, token): + try: + verification_token = EmailVerificationToken.objects.get(token=token) + if verification_token.is_valid(): + user = verification_token.user + user.is_active = True + user.save() + verification_token.delete() + return Response({'message': '이메일이 성공적으로 인증되었습니다.'}, status=status.HTTP_200_OK) + else: + return Response({'error': '유효하지 않은 토큰입니다.'}, status=status.HTTP_400_BAD_REQUEST) + except EmailVerificationToken.DoesNotExist: + return Response({'error': '유효하지 않은 토큰입니다.'}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file From 512f81d0ce31409209e80c3b5e889b95bcf330c4 Mon Sep 17 00:00:00 2001 From: Moon-Nights Date: Tue, 30 Jul 2024 10:14:18 +0900 Subject: [PATCH 2/3] naengttogi-django-common-login-feature-added --- Django/app/settings.py | 12 +- Django/app/urls.py | 1 - Django/app_user/models.py | 11 ++ .../{common_user => app_user}/serializers.py | 33 +++-- Django/app_user/urls.py | 10 ++ Django/app_user/views.py | 125 +++++++++++++++++- Django/common_user/__init__.py | 0 Django/common_user/admin.py | 3 - Django/common_user/apps.py | 6 - Django/common_user/migrations/__init__.py | 0 Django/common_user/models.py | 58 -------- Django/common_user/tests.py | 3 - Django/common_user/urls.py | 8 -- Django/common_user/utils.py | 10 -- Django/common_user/views.py | 78 ----------- 15 files changed, 172 insertions(+), 186 deletions(-) rename Django/{common_user => app_user}/serializers.py (50%) delete mode 100644 Django/common_user/__init__.py delete mode 100644 Django/common_user/admin.py delete mode 100644 Django/common_user/apps.py delete mode 100644 Django/common_user/migrations/__init__.py delete mode 100644 Django/common_user/models.py delete mode 100644 Django/common_user/tests.py delete mode 100644 Django/common_user/urls.py delete mode 100644 Django/common_user/utils.py delete mode 100644 Django/common_user/views.py diff --git a/Django/app/settings.py b/Django/app/settings.py index 67b3b43..985d385 100644 --- a/Django/app/settings.py +++ b/Django/app/settings.py @@ -65,7 +65,6 @@ "allauth.socialaccount", "allauth.socialaccount.providers.google", # Google provider 추가 # # Common Login - "common_user", ] @@ -213,23 +212,24 @@ -# # Common Login -# AUTH_USER_MODEL = "common_user.commonUser" # Added Custom User +# added common Login JWT REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), } - +# JWT Settings from datetime import timedelta SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, } -# 이메일 설정 +# e-mail settings (ex1. used gmail) EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.gmail.com' # Gmail SMTP 서버 사용 EMAIL_PORT = 587 diff --git a/Django/app/urls.py b/Django/app/urls.py index 231bfe4..a8227f7 100644 --- a/Django/app/urls.py +++ b/Django/app/urls.py @@ -47,5 +47,4 @@ ), name="schema-redoc", ), - path('api/common_user/', include('common_users.urls')), # added common_user ] diff --git a/Django/app_user/models.py b/Django/app_user/models.py index fd3ced9..322e908 100644 --- a/Django/app_user/models.py +++ b/Django/app_user/models.py @@ -3,6 +3,8 @@ from django.contrib.auth.models import Permission from django.db import models from django.utils.translation import gettext_lazy as _ +from django.utils.crypto import get_random_string # added common user +# from django.utils import timezone class UserRole(models.TextChoices): @@ -21,6 +23,9 @@ class App_User(AbstractUser): is_activate = models.BooleanField(default=True) role = models.CharField( max_length=10, choices=UserRole.choices, default=UserRole.USER + is_email_verified = models.BooleanField(default=False) # added common user email verified + email_verification_token = models.CharField(max_length=100, blank=True) # added common user email token + email = models.EmailField(unique=True) ) USERNAME_FIELD = "user_id" # user_id를 사용자 이름으로 사용 @@ -29,6 +34,7 @@ class App_User(AbstractUser): "nick_name", "password_hash", "username", + 'email', # added common login email ] # 필수 필드 설정 groups = models.ManyToManyField( Group, verbose_name="groups", blank=True, related_name="app_user_set" @@ -42,3 +48,8 @@ class App_User(AbstractUser): def __str__(self): return self.user_id + + # added generate email token + def generate_email_token(self): + self.email_verification_token = get_random_string(length=32) + self.save() diff --git a/Django/common_user/serializers.py b/Django/app_user/serializers.py similarity index 50% rename from Django/common_user/serializers.py rename to Django/app_user/serializers.py index 8aa1c10..f2c3424 100644 --- a/Django/common_user/serializers.py +++ b/Django/app_user/serializers.py @@ -1,6 +1,5 @@ from rest_framework import serializers -from django.contrib.auth import get_user_model -from django.core.exceptions import ValidationError +from django.contrib.auth import get_user_model, authenticate from django.contrib.auth.password_validation import validate_password User = get_user_model() @@ -19,19 +18,29 @@ def validate(self, attrs): return attrs def create(self, validated_data): - user = User.objects.create_user( - email=validated_data['email'], + user = User.objects.create( username=validated_data['username'], - nickname=validated_data['nickname'], - password=validated_data['password'] + email=validated_data['email'], + nickname=validated_data['nickname'] ) + user.set_password(validated_data['password']) + user.save() return user class UserLoginSerializer(serializers.Serializer): - username_or_email = serializers.CharField(required=True) - password = serializers.CharField(required=True) + username = serializers.CharField(required=False) + email = serializers.EmailField(required=False) + password = serializers.CharField() -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ('id', 'email', 'username', 'nickname') \ No newline at end of file + def validate(self, data): + if not data.get('username') and not data.get('email'): + raise serializers.ValidationError("Must include either 'username' or 'email'.") + return data + +# added common user Password Reset +class PasswordResetRequestSerializer(serializers.Serializer): + email = serializers.EmailField() + +class PasswordResetConfirmSerializer(serializers.Serializer): + token = serializers.CharField() + new_password = serializers.CharField(write_only=True, validators=[validate_password]) \ No newline at end of file diff --git a/Django/app_user/urls.py b/Django/app_user/urls.py index bdbf50a..25fc712 100644 --- a/Django/app_user/urls.py +++ b/Django/app_user/urls.py @@ -2,6 +2,8 @@ from django.urls import include from django.urls import path from rest_framework.routers import DefaultRouter +from .views import (UserRegistrationView, UserLoginView, EmailVerificationView, + PasswordResetRequestView, PasswordResetConfirmView, UserLogoutView) # import added common user from . import views @@ -23,4 +25,12 @@ urlpatterns = [ path("", include(router.urls)), + path('register/', UserRegistrationView.as_view(), name='user-register'), + # added common login user-register path + path('login/', UserLoginView.as_view(), name='user-login'), + # added common login user-login path + path('verify-email//', EmailVerificationView.as_view(), name='verify-email'), + path('password-reset/', PasswordResetRequestView.as_view(), name='password-reset-request'), + path('password-reset/confirm/', PasswordResetConfirmView.as_view(), name='password-reset-confirm'), + path('logout/', UserLogoutView.as_view(), name='user-logout'), ] diff --git a/Django/app_user/views.py b/Django/app_user/views.py index 2a4673a..8b73847 100644 --- a/Django/app_user/views.py +++ b/Django/app_user/views.py @@ -6,6 +6,20 @@ from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.routers import DefaultRouter +# import added common login +from django.shortcuts import render +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework_simplejwt.tokens import RefreshToken +from django.contrib.auth import authenticate, get_user_model +from django.core.mail import send_mail +from django.conf import settings +# from .models import EmailVerificationToken +# from .utils import send_verification_email +from django.utils.crypto import get_random_string +from .serializers import (UserRegistrationSerializer, UserLoginSerializer, + PasswordResetRequestSerializer, PasswordResetConfirmSerializer) class UserLoginViewSet(viewsets.GenericViewSet): @@ -282,6 +296,115 @@ def reset_password(self, request): return Response({"message": "비밀번호 재설정 API (미구현)"}) +# added Common User + +User = get_user_model() + + +class UserRegistrationView(APIView): + def post(self, request): + serializer = UserRegistrationSerializer(data=request.data) + if serializer.is_valid(): + user = serializer.save() + user.generate_email_token() + + # Send verification email + subject = 'Verify your email' + message = f'Please click the link to verify your email: http://yourdomain.com/verify-email/{user.email_verification_token}' + send_mail(subject, message, settings.EMAIL_HOST_USER, [user.email]) + + return Response({ + 'message': 'User registered successfully. Please check your email to verify your account.', + 'user_id': user.id, + 'username': user.username, + 'email': user.email, + }, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class UserLoginView(APIView): + def post(self, request): + serializer = UserLoginSerializer(data=request.data, context={'request': request}) + if serializer.is_valid(): + user = serializer.validated_data['user'] + if not user.is_email_verified: + return Response({'error': 'Please verify your email before logging in.'}, + status=status.HTTP_403_FORBIDDEN) + + refresh = RefreshToken.for_user(user) + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + 'user_id': user.id, + 'username': user.username, + 'email': user.email, + }) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class EmailVerificationView(APIView): + def get(self, request, token): + try: + user = User.objects.get(email_verification_token=token) + user.is_email_verified = True + user.email_verification_token = '' + user.save() + return Response({'message': 'Email verified successfully'}, status=status.HTTP_200_OK) + except User.DoesNotExist: + return Response({'error': 'Invalid token'}, status=status.HTTP_400_BAD_REQUEST) + + +class UserLogoutView(APIView): + permission_classes = (IsAuthenticated,) + + def post(self, request): + try: + refresh_token = request.data["refresh_token"] + token = RefreshToken(refresh_token) + token.blacklist() + return Response({'message': 'Successfully logged out.'}, status=status.HTTP_200_OK) + except Exception as e: + return Response({'error': 'Invalid token.'}, status=status.HTTP_400_BAD_REQUEST) + + +class PasswordResetRequestView(APIView): + def post(self, request): + serializer = PasswordResetRequestSerializer(data=request.data) + if serializer.is_valid(): + email = serializer.validated_data['email'] + try: + user = User.objects.get(email=email) + token = get_random_string(length=32) + user.email_verification_token = token + user.save() + + # Send password reset email + subject = 'Password Reset' + message = f'Use this token to reset your password: {token}' + send_mail(subject, message, settings.EMAIL_HOST_USER, [email]) + + return Response({'message': 'Password reset email sent'}, status=status.HTTP_200_OK) + except User.DoesNotExist: + return Response({'error': 'User with this email does not exist'}, status=status.HTTP_404_NOT_FOUND) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class PasswordResetConfirmView(APIView): + def post(self, request): + serializer = PasswordResetConfirmSerializer(data=request.data) + if serializer.is_valid(): + token = serializer.validated_data['token'] + new_password = serializer.validated_data['new_password'] + try: + user = User.objects.get(email_verification_token=token) + user.set_password(new_password) + user.email_verification_token = '' + user.save() + return Response({'message': 'Password reset successfully'}, status=status.HTTP_200_OK) + except User.DoesNotExist: + return Response({'error': 'Invalid token'}, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + class UserRefrigeratorViewSet(viewsets.ViewSet): # ListModelMixin 제거 """ 유저 냉장고 목록 관련 API @@ -318,4 +441,4 @@ def list_refrigerators(self, request): """ 유저 냉장고 목록 불러오기 """ - return Response({"message": "유저 냉장고 목록 불러오기 API (미구현)"}) + return Response({"message": "유저 냉장고 목록 불러오기 API (미구현)"}) \ No newline at end of file diff --git a/Django/common_user/__init__.py b/Django/common_user/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Django/common_user/admin.py b/Django/common_user/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/Django/common_user/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/Django/common_user/apps.py b/Django/common_user/apps.py deleted file mode 100644 index 124a32a..0000000 --- a/Django/common_user/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class CommonUserConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'common_user' diff --git a/Django/common_user/migrations/__init__.py b/Django/common_user/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Django/common_user/models.py b/Django/common_user/models.py deleted file mode 100644 index 6900fae..0000000 --- a/Django/common_user/models.py +++ /dev/null @@ -1,58 +0,0 @@ -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin -from django.db import models -from django.utils.translation import gettext_lazy as _ -from django.utils import timezone - - -# Create your models here. - -import secrets - -class Common_user(AbstractUser): - - def create_user(self, email, username, nickname, password=None): - if not email: - raise ValueError(_('The Email field must be set')) - email = self.normalize_email(email) - user = self.model(email=email, username=username, nickname=nickname) - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, email, username, nickname, password): - user = self.create_user(email, username, nickname, password) - user.is_staff = True - user.is_superuser = True - user.save(using=self._db) - return user - - - class Common_user(AbstractBaseUser, PermissionsMixin): - email = models.EmailField(_('email address'), unique=True) - username = models.CharField(_('username'), max_length=30, unique=True) - nickname = models.CharField(_('nickname'), max_length=30) - is_active = models.BooleanField(default=True) - is_staff = models.BooleanField(default=False) - date_joined = models.DateTimeField(auto_now_add=True) - - objects = Common_userManager() - - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['email', 'nickname'] - - def __str__(self): - return self.email - -# Create Token - class EmailVerificationToken(models.Model): - user = models.OneToOneField(CustomUser, on_delete=models.CASCADE) - token = models.CharField(max_length=64, unique=True) - created_at = models.DateTimeField(auto_now_add=True) - - def save(self, *args, **kwargs): - if not self.token: - self.token = secrets.token_urlsafe(32) - super().save(*args, **kwargs) - - def is_valid(self): - return (timezone.now() - self.created_at).days < 1 diff --git a/Django/common_user/tests.py b/Django/common_user/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/Django/common_user/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Django/common_user/urls.py b/Django/common_user/urls.py deleted file mode 100644 index 78323f5..0000000 --- a/Django/common_user/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import path -from .views import UserRegistrationView, UserLoginView, VerifyEmailView - -urlpatterns = [ - path('register/', UserRegistrationView.as_view(), name='register'), - path('login/', UserLoginView.as_view(), name='login'), - path('verify-email//', VerifyEmailView.as_view(), name='verify_email'), -] \ No newline at end of file diff --git a/Django/common_user/utils.py b/Django/common_user/utils.py deleted file mode 100644 index a4ace47..0000000 --- a/Django/common_user/utils.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.core.mail import send_mail -from django.conf import settings - -# 이메일 전송 함수 -def send_verification_email(user, token): - subject = '이메일 주소 확인' - message = f'다음 링크를 클릭하여 이메일 주소를 확인해주세요: http://yourdomain.com/verify-email/{token}' - email_from = settings.EMAIL_HOST_USER - recipient_list = [user.email,] - send_mail(subject, message, email_from, recipient_list) \ No newline at end of file diff --git a/Django/common_user/views.py b/Django/common_user/views.py deleted file mode 100644 index 3243b33..0000000 --- a/Django/common_user/views.py +++ /dev/null @@ -1,78 +0,0 @@ -from django.shortcuts import render -from rest_framework import status -from rest_framework.response import Response -from rest_framework.views import APIView -from rest_framework_simplejwt.tokens import RefreshToken -from django.contrib.auth import authenticate, get_user_model -from .serializers import UserRegistrationSerializer, UserLoginSerializer, UserSerializer -from .models import EmailVerificationToken -from .utils import send_verification_email - -# Create your views here. - -User = get_user_model() - -class UserRegistrationView(APIView): - def post(self, request): - serializer = UserRegistrationSerializer(data=request.data) - if serializer.is_valid(): - user = serializer.save() - user.is_active = False # 이메일 인증 전까지 계정을 비활성화 처리 - user.save() - refresh = RefreshToken.for_user(user) - token = EmailVerificationToken.objects.create(user=user) # 이메일 인증 토큰 생성 - send_verification_email(user, token.token) # 인증 이메일 발송 - return Response({ - 'refresh': str(refresh), - 'access': str(refresh.access_token), - 'message': '회원가입이 완료되었습니다. 이메일을 확인하여 계정을 활성화해주세요.', - 'user': UserSerializer(user).data - }, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class UserLoginView(APIView): - def post(self, request): - serializer = UserLoginSerializer(data=request.data) - if serializer.is_valid(): - username_or_email = serializer.validated_data['username_or_email'] - password = serializer.validated_data['password'] - - # 이메일 또는 아이디로 로그인 시도 - user = None - if '@' in username_or_email: - try: - user = User.objects.get(email=username_or_email) - except User.DoesNotExist: - pass - - if not user: - user = authenticate(username=username_or_email, password=password) - else: - if not user.check_password(password): - user = None - - if user: - refresh = RefreshToken.for_user(user) - return Response({ - 'refresh': str(refresh), - 'access': str(refresh.access_token), - 'user': UserSerializer(user).data - }) - return Response({'error': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - class VerifyEmailView(APIView): - def get(self, request, token): - try: - verification_token = EmailVerificationToken.objects.get(token=token) - if verification_token.is_valid(): - user = verification_token.user - user.is_active = True - user.save() - verification_token.delete() - return Response({'message': '이메일이 성공적으로 인증되었습니다.'}, status=status.HTTP_200_OK) - else: - return Response({'error': '유효하지 않은 토큰입니다.'}, status=status.HTTP_400_BAD_REQUEST) - except EmailVerificationToken.DoesNotExist: - return Response({'error': '유효하지 않은 토큰입니다.'}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file From 6d7dc7bf7bc635dd38caea00cad974cc298eadf1 Mon Sep 17 00:00:00 2001 From: Moon-Nights Date: Fri, 2 Aug 2024 23:29:47 +0900 Subject: [PATCH 3/3] update common-login+code_cleanup+swagger --- Django/app/settings.py | 8 +++ Django/app_user/models.py | 6 +- Django/app_user/serializers.py | 12 +++- Django/app_user/views.py | 123 ++++++++++++++++++++++++--------- 4 files changed, 111 insertions(+), 38 deletions(-) diff --git a/Django/app/settings.py b/Django/app/settings.py index ae67d23..719fbfb 100644 --- a/Django/app/settings.py +++ b/Django/app/settings.py @@ -208,3 +208,11 @@ "USE_SESSION_AUTH": False, # 세션 인증 비활성화 (JWT 사용 시) "JSON_EDITOR": True, # JSON 형식 편집기 활성화 (선택 사항) } + +# # 이메일 설정 (예: Gmail 사용) +# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +# EMAIL_HOST = 'smtp.gmail.com' +# EMAIL_PORT = 587 +# EMAIL_USE_TLS = True +# EMAIL_HOST_USER = 'your-email@gmail.com' +# EMAIL_HOST_PASSWORD = 'your-email-password' \ No newline at end of file diff --git a/Django/app_user/models.py b/Django/app_user/models.py index 1c30f05..4d1817d 100644 --- a/Django/app_user/models.py +++ b/Django/app_user/models.py @@ -9,8 +9,8 @@ from django.contrib.auth.models import PermissionsMixin from django.db import models from django.utils.translation import gettext_lazy as _ -from django.utils.crypto import get_random_string # added common user -# from django.utils import timezone +from django.utils.crypto import get_random_string # added common user login dependency +# from django.utils import timezone # added common user login dependency from rest_framework_simplejwt.tokens import OutstandingToken @@ -126,7 +126,7 @@ def decrypt_refresh_token(self): def __str__(self): return self.user_id - # added generate email token + # added common user generate email token def generate_email_token(self): self.email_verification_token = get_random_string(length=32) self.save() diff --git a/Django/app_user/serializers.py b/Django/app_user/serializers.py index 1696472..d6ca494 100644 --- a/Django/app_user/serializers.py +++ b/Django/app_user/serializers.py @@ -7,7 +7,9 @@ class UserRegistrationSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) password2 = serializers.CharField(write_only=True, required=True) - + """ + added common user sign-up registration + """ class Meta: model = User fields = ('email', 'username', 'nickname', 'password', 'password2') @@ -31,13 +33,14 @@ class UserLoginSerializer(serializers.Serializer): username = serializers.CharField(required=False) email = serializers.EmailField(required=False) password = serializers.CharField() - + """ + added common user sign-in + """ def validate(self, data): if not data.get('username') and not data.get('email'): raise serializers.ValidationError("Must include either 'username' or 'email'.") return data -# added common user Password Reset class PasswordResetRequestSerializer(serializers.Serializer): email = serializers.EmailField() @@ -47,6 +50,9 @@ class PasswordResetConfirmSerializer(serializers.Serializer): from rest_framework_simplejwt.serializers import TokenRefreshSerializer +""" +added common user password reset management +""" class CustomTokenObtainPairSerializer(TokenRefreshSerializer): """ diff --git a/Django/app_user/views.py b/Django/app_user/views.py index a024aa5..20b8247 100644 --- a/Django/app_user/views.py +++ b/Django/app_user/views.py @@ -1,51 +1,43 @@ import os - import requests from app_user.models import App_User from django.conf import settings -from django.contrib.auth import authenticate -from django.contrib.auth import get_user_model -from django.contrib.auth import login +from django.contrib.auth import authenticate, login, get_user_model from django.db import transaction -from django.http import HttpResponse -from django.http import JsonResponse -from django.shortcuts import redirect -from django.views import View +from django.http import HttpResponse, JsonResponse +from django.shortcuts import redirect, render +from django.core.mail import send_mail +from django.utils.crypto import get_random_string + from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema -from rest_framework import status -from rest_framework import viewsets +from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.routers import DefaultRouter -# import added common login -from django.shortcuts import render -from rest_framework import status -from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated -from rest_framework_simplejwt.tokens import RefreshToken -from django.contrib.auth import authenticate, get_user_model -from django.core.mail import send_mail -from django.conf import settings -# from .models import EmailVerificationToken -# from .utils import send_verification_email -from django.utils.crypto import get_random_string -from .serializers import (UserRegistrationSerializer, UserLoginSerializer, - PasswordResetRequestSerializer, PasswordResetConfirmSerializer) -from rest_framework.test import APIRequestFactory from rest_framework.views import APIView from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework_simplejwt.exceptions import TokenError from rest_framework_simplejwt.serializers import TokenRefreshSerializer -from rest_framework_simplejwt.settings import api_settings from rest_framework_simplejwt.tokens import RefreshToken -from rest_framework_simplejwt.views import TokenBlacklistView -from rest_framework_simplejwt.views import TokenObtainPairView -from rest_framework_simplejwt.views import TokenRefreshView +from rest_framework_simplejwt.views import TokenBlacklistView, TokenObtainPairView, TokenRefreshView + +from .serializers import (UserRegistrationSerializer, UserLoginSerializer, + PasswordResetRequestSerializer, PasswordResetConfirmSerializer, + CustomTokenObtainPairSerializer) -from .serializers import CustomTokenObtainPairSerializer +""" +duplicate import code clean legibility + +- django.contrib.auth에서 authenticate, login, get_user_model을 한 줄로 정리 +- django.http에서 HttpResponse, JsonResponse를 한 줄로 정리 +- django.shortcuts에서 redirect, render를 한 줄로 정리 +- rest_framework에서 status, viewsets를 한 줄로 정리 +- rest_framework_simplejwt.tokens에서 RefreshToken을 중복 제거 + +""" User = get_user_model() state = os.environ.get("STATE") @@ -601,12 +593,22 @@ def reset_password(self, request): return Response({"message": "비밀번호 재설정 API (미구현)"}) -# added Common User +# added Common User code fix update swagger User = get_user_model() class UserRegistrationView(APIView): + @swagger_auto_schema( + request_body=UserRegistrationSerializer, + responses={ + 201: openapi.Response('User registered successfully'), + 400: openapi.Response('Invalid data'), + }, + tags=["User"], + operation_summary="사용자 등록", + operation_description="사용자를 등록하고 이메일 확인을 위한 토큰을 발송합니다." + ) def post(self, request): serializer = UserRegistrationSerializer(data=request.data) if serializer.is_valid(): @@ -615,7 +617,7 @@ def post(self, request): # Send verification email subject = 'Verify your email' - message = f'Please click the link to verify your email: http://yourdomain.com/verify-email/{user.email_verification_token}' + message = f'Please click the link to verify your email: {user.email_verification_token}' send_mail(subject, message, settings.EMAIL_HOST_USER, [user.email]) return Response({ @@ -628,6 +630,17 @@ def post(self, request): class UserLoginView(APIView): + @swagger_auto_schema( + request_body=UserLoginSerializer, + responses={ + 200: openapi.Response('User logged in successfully'), + 403: openapi.Response('Email not verified'), + 400: openapi.Response('Invalid data'), + }, + tags=["User"], + operation_summary="사용자 로그인", + operation_description="이메일 확인 후 사용자가 로그인할 수 있도록 합니다." + ) def post(self, request): serializer = UserLoginSerializer(data=request.data, context={'request': request}) if serializer.is_valid(): @@ -648,6 +661,15 @@ def post(self, request): class EmailVerificationView(APIView): + @swagger_auto_schema( + responses={ + 200: openapi.Response('Email verified successfully'), + 400: openapi.Response('Invalid token'), + }, + tags=["User"], + operation_summary="이메일 인증", + operation_description="이메일 인증 토큰을 사용하여 사용자의 이메일을 인증합니다." + ) def get(self, request, token): try: user = User.objects.get(email_verification_token=token) @@ -662,6 +684,21 @@ def get(self, request, token): class UserLogoutView(APIView): permission_classes = (IsAuthenticated,) + @swagger_auto_schema( + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'refresh_token': openapi.Schema(type=openapi.TYPE_STRING) + } + ), + responses={ + 200: openapi.Response('Successfully logged out'), + 400: openapi.Response('Invalid token'), + }, + tags=["User"], + operation_summary="사용자 로그아웃", + operation_description="사용자가 로그아웃합니다." + ) def post(self, request): try: refresh_token = request.data["refresh_token"] @@ -673,6 +710,17 @@ def post(self, request): class PasswordResetRequestView(APIView): + @swagger_auto_schema( + request_body=PasswordResetRequestSerializer, + responses={ + 200: openapi.Response('Password reset email sent'), + 404: openapi.Response('User with this email does not exist'), + 400: openapi.Response('Invalid data'), + }, + tags=["User"], + operation_summary="비밀번호 재설정 요청", + operation_description="사용자의 이메일로 비밀번호 재설정 토큰을 발송합니다." + ) def post(self, request): serializer = PasswordResetRequestSerializer(data=request.data) if serializer.is_valid(): @@ -695,6 +743,16 @@ def post(self, request): class PasswordResetConfirmView(APIView): + @swagger_auto_schema( + request_body=PasswordResetConfirmSerializer, + responses={ + 200: openapi.Response('Password reset successfully'), + 400: openapi.Response('Invalid token'), + }, + tags=["User"], + operation_summary="비밀번호 재설정 확인", + operation_description="재설정 토큰을 사용하여 비밀번호를 변경합니다." + ) def post(self, request): serializer = PasswordResetConfirmSerializer(data=request.data) if serializer.is_valid(): @@ -710,6 +768,7 @@ def post(self, request): return Response({'error': 'Invalid token'}, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + class UserRefrigeratorViewSet(viewsets.ViewSet): # ListModelMixin 제거 """ 유저 냉장고 목록 관련 API