diff --git a/drfTuto/custom_renderer.py b/drfTuto/custom_renderer.py new file mode 100644 index 0000000..994e8a8 --- /dev/null +++ b/drfTuto/custom_renderer.py @@ -0,0 +1,13 @@ +from rest_framework import renderers + +class CustomRenderer(renderers.BaseRenderer): + + def render(self, data, accepted_media_type=None, renderer_context=None): + response_data = renderer_context.get('response') + + response = { + 'status': response_data.status_text, + 'data': data + } + + return super(CustomRenderer, self).render(response, accepted_media_type, renderer_context) \ No newline at end of file diff --git a/drfTuto/settings.py b/drfTuto/settings.py index c11d7ad..5fd57b8 100644 --- a/drfTuto/settings.py +++ b/drfTuto/settings.py @@ -22,7 +22,7 @@ # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-6d7u2b(=#!%%$%ty=3juq^s+(&d)fzo)!i!4v21nx4991c7f1j' +SECRET_KEY = os.environ.get('Secret_Key') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -40,6 +40,8 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'django_filters', + 'rest_framework_simplejwt' ] + PSJ_APPS @@ -77,14 +79,26 @@ # REST FRAMEWORK REST_FRAMEWORK = { + ''' 'DEFAULT_RENDERER_CLASSES': ( 'djangorestframework_camel_case.render.CamelCaseJSONRenderer', 'djangorestframework_camel_case.render.CamelCaseBrowsableAPIRenderer', ), + ''' + 'DEFAULT_RENDERER_CLASSES': ( + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ), 'DEFAULT_PARSER_CLASSES': ( 'djangorestframework_camel_case.parser.CamelCaseFormParser', 'djangorestframework_camel_case.parser.CamelCaseMultiPartParser', 'djangorestframework_camel_case.parser.CamelCaseJSONParser', + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + "EXCEPTION_HANDLER": "user.utils.custom_exception_handler", + "DEFAULT_FILTER_BACKENDS": 'django_filters.rest_framework.DjangoFilterBackend', } # Database @@ -97,6 +111,8 @@ } } +LOGIN_REDIRECT_URL = '/' #로그인시 이동하는 페이지 +LOGOUT_REDIRECT_URL = '/' #로그아웃시 이동하는 페이지 # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators diff --git a/drfTuto/urls.py b/drfTuto/urls.py index 789b0d0..4310ec8 100644 --- a/drfTuto/urls.py +++ b/drfTuto/urls.py @@ -18,5 +18,6 @@ urlpatterns = [ path('admin/', admin.site.urls), - path('users/', include('user.urls')), + path('user/', include('user.urls')), + #path("api-auth/", include("rest_framework.urls")), ] \ No newline at end of file diff --git a/drfTuto/utils.py b/drfTuto/utils.py new file mode 100644 index 0000000..e263863 --- /dev/null +++ b/drfTuto/utils.py @@ -0,0 +1,40 @@ +from rest_framework.views import exception_handler + +class PasswordNotMatch(APIException): + status_code = 400 + default_detail = "Passwords does not match." + default_code = "bad_request" + + +def custom_exception_handler(exc, context): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc, context) + + # Now add the HTTP status code to the response. + if response is not None: + if isinstance(exc, Http404): + customized_response = {"code": response.status_code, "details": "Not Found"} + elif isinstance(exc, exceptions.NotFound): + customized_response = {"code": response.status_code, "details": exc.detail} + elif isinstance(exc, exceptions.MethodNotAllowed): + customized_response = {"code": response.status_code, "details": exc.detail} + elif isinstance(exc, exceptions.NotAcceptable): + customized_response = {"code": response.status_code, "details": exc.detail} + elif isinstance(exc, exceptions.UnsupportedMediaType): + customized_response = {"code": response.status_code, "details": exc.detail} + elif isinstance(exc, exceptions.AuthenticationFailed): + customized_response = {"code": response.status_code, "details": exc.detail} + elif isinstance(exc, exceptions.PermissionDenied): + customized_response = {"code": response.status_code, "details": exc.detail} + elif isinstance(exc, exceptions.NotAuthenticated): + customized_response = {"code": response.status_code, "details": exc.detail} + else: + customized_response = { + "code": response.status_code, + "details": response.data, + } + + response.data = customized_response + + return response \ No newline at end of file diff --git a/user/auth_urls.py b/user/auth_urls.py new file mode 100644 index 0000000..50f1982 --- /dev/null +++ b/user/auth_urls.py @@ -0,0 +1,30 @@ +from django.urls import path +from . import auth_views +from django.contrib.auth import views +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, +) + +urlpatterns = [ + path("signup/", auth_views.SignUpView.as_view(), name="signup"), + path('login/', views.LoginView.as_view(), name='login'), + path('logout/', views.LogoutView.as_view(), name='logout'), +] + +urlpatterns += [ + path( + "check-email/", + auth_views.CheckDuplicateUsernameView.as_view(), + name="check-email", + ), + path("email-verification/", auth_views.EmailVerification.as_view(), name="verify-email"), + path('email-confirmation/', auth_views.EmailConfirmation.as_view(), name="activate"), + path( + "password-change/", + auth_views.PasswordChangeView.as_view(), + name="password-change", + ), + path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), +] \ No newline at end of file diff --git a/user/auth_views.py b/user/auth_views.py new file mode 100644 index 0000000..fcf6fb0 --- /dev/null +++ b/user/auth_views.py @@ -0,0 +1,130 @@ +from django.contrib.auth.models import update_last_login +from django.http import JsonResponse +from django.shortcuts import get_object_or_404 +from rest_framework.permissions import AllowAny +from rest_framework.views import APIView +from rest_framework.exceptions import AuthenticationFailed + +from drfTuto.utils import PasswordNotMatch +from drfTuto.custom_renderer import CustomRenderer +from user.models import User +from rest_framework.response import Response +from rest_framework import status, generics, permissions +from django.contrib.auth.hashers import check_password + +from user.serializers import UserSerializer +from user.gen_codes import GenerateCode + +import smtplib +from email.mime.text import MIMEText + +from dotenv import load_dotenv +import os + +smtp_info = { + 'gmail.com': ('smtp.gmail.com', 587), + 'naver.com': ('smtp.naver.com', 587), +} + +smtp = smtplib.SMTP('smtp.gmail.com', 587) + +smtp.ehlo() + +smtp.starttls() #tls 암호화, 587 port 사용시 필요 + + +class BasicSignUpView(APIView): + serializer_class = UserSerializer + renderer_classes = [CustomRenderer] + permission_classes = [AllowAny] + + def post(self, request, *args, **kwargs): + + password = request.data.get("password") + confirm_password = request.data.get("confirm_password") + + if password != confirm_password: + raise PasswordNotMatch + + email = request.data.get("email") + nickname = request.data.get("nickname") + + user = User.objects.create_user(email=email, password=password, nickname=nickname) + + serializer = UserSerializer(user) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + +class PasswordChangeView(APIView): + serializer = UserSerializer + renderer_classes = [CustomRenderer] + + def post(self, request, *args, **kwargs): + user = request.user + current_password = request.data.get("current_password") + new_password = request.data.get("new_password") + + if not check_password(current_password, user.password): + raise AuthenticationFailed + + user.set_password(new_password) + user.save(update_fields=["password"]) + + serializer = UserSerializer(user) + return Response(serializer.data, status=status.HTTP_200_OK) + +class CheckDuplicateUsernameView(APIView): + renderer_classes = [CustomRenderer] + permission_classes = [AllowAny] + + def post(self, request, *args, **kwargs): + email = request.data.get("email") + + existing_email = User.objects.filter(email=email).first() + if existing_email: + return Response({"details": "Provided email already exists."}, status=status.HTTP_409_CONFLICT) + + return Response({"email": email}, status=status.HTTP_200_OK) + +class EmailVerification(APIView): + renderer_classes = [CustomRenderer] + permission_classes = [AllowAny] + + def post(self, request, *args, **kwargs): + email = request.data.get("email") + generated_code = GenerateCode.generate_random_code() + + # set code in cookie + res = JsonResponse({'success': True}) + res.set_cookie('email_verification_code', generated_code, max_age=300) + + # 메일 내용 + msg = MIMEText('이메일 인증 코드 : ', generated_code) + + # 메일 제목 + msg['Subject'] = '이메일 인증 코드입니다' + + success = smtp.sendmail('june416412@gmail.com', email, msg.as_string()) #발신 메일, 수신 메일, 본문 내용 + + if success > 0: + return Response(status=status.HTTP_200_OK) + elif success == 0: + return Response({"details": "Failed to send email"},status=status.HTTP_400_BAD_REQUEST) + + +class EmailConfirmation(APIView): + permission_classes = [AllowAny] + + def post(self, request, *args, **kwargs): + if 'email_verification_code' in request.COOKIES: + code_cookie = request.COOKIES['email_verification_code'] + else: + return Response({"details": "No cookies attached"}, status=status.HTTP_400_BAD_REQUEST) + + code_input = request.data.get("verification_code") + if code_cookie == code_input: + return Response(status=status.HTTP_200_OK) + else: + return Response({"details": "Verification code does not match."}, status=status.HTTP_400_BAD_REQUEST) + +smtp.login('june416412@gmail.com', os.environ.get('EMAIL_LOGIN_KEY')) \ No newline at end of file diff --git a/user/gen_codes.py b/user/gen_codes.py new file mode 100644 index 0000000..b9e3032 --- /dev/null +++ b/user/gen_codes.py @@ -0,0 +1,16 @@ +import string +import random + +class GenerateCode: + @staticmethod + def deactivate_user(user): + user.is_active = False + user.save(update_fields=["is_active"]) + return user + + @staticmethod + def generate_random_code(): + number_of_strings = 5 + length_of_string = 8 + for x in range(number_of_strings): + return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length_of_string)) \ No newline at end of file diff --git a/user/generics_views.py b/user/generics_views.py index 182d6f5..b0246c5 100644 --- a/user/generics_views.py +++ b/user/generics_views.py @@ -1,20 +1,21 @@ from user.models import MyUser from user.serializers import UserSerializer -from rest_framework import generics, mixins +from rest_framework import generics, mixins, filters from rest_framework.response import Response from datetime import datetime +from rest_framework import status + class UserList(generics.ListCreateAPIView): - queryset = MyUser.objects.all() + queryset = MyUser.objects.filter(is_active=True).all() serializer_class = UserSerializer + filter_backends = [filters.SearchFilter] + search_fields = ["nickname", "email"] + +class UserDetail(generics.RetrieveUpdateDestroyAPIView): -class UserDetail(generics.RetireveAPIView): #검색 - queryset = MyUser.objects.all() - serialzier_class = UserSerializer - -class UserUpdate(generics.UpdateAPIView): #업데이트 queryset = MyUser.objects.all() - serialzier_class = UserSerializer + serializer_class = UserSerializer def update(self, request, *args, **kwargs): partial = kwargs.pop('partial', False) @@ -26,11 +27,7 @@ def update(self, request, *args, **kwargs): serializer.save( updated_at=datetime.now() ) - return Response(serializer.data, status=status.HTTP_200_OK) - -class UserDestroy(generics.DestroyAPIView): - queryset = MyUser.objects.all() - serialzier_class = UserSerializer + return Response(serializer.dwsata, status=status.HTTP_200_OK) def delete(self, request, *args, **kwargs): instance = self.get_object() diff --git a/user/urls.py b/user/urls.py index 6517ecb..1ad2d84 100644 --- a/user/urls.py +++ b/user/urls.py @@ -1,8 +1,7 @@ from django.urls import path -from rest_framework.urlpatterns import format_suffix_patterns -from user.views import UserView +from user import generics_views urlpatterns = [ - #path('', views.UserList.as_view()), - #path('/', views.UserDetail.as_view()), + path("", generics_views.UserList.as_view(), name="user_list"), + path("/", generics_views.UserDetail.as_view(), name="user_detail"), ] \ No newline at end of file diff --git a/user/views.py b/user/views.py index 9322044..bd8d473 100644 --- a/user/views.py +++ b/user/views.py @@ -8,4 +8,5 @@ class UserView(viewsets.ModelViewSet): A simple ViewSet for viewing and editing accounts. """ queryset = MyUser.objects.all() - serializer_class = UserSerializer \ No newline at end of file + serializer_class = UserSerializer + diff --git a/user/viewsets_urls.py b/user/viewsets_urls.py new file mode 100644 index 0000000..a55f617 --- /dev/null +++ b/user/viewsets_urls.py @@ -0,0 +1,18 @@ +from django.urls import path, include +from rest_framework.urlpatterns import format_suffix_patterns +from user.views import UserView + +user_list = UserView.as_view({ + 'get': 'list', + 'post': 'create' +}) + +user_detail = UserView.as_view({ + 'get': 'retrieve', + 'patch': 'partial_update', +}) + +urlpatterns = format_suffix_patterns([ + path("/", user_list, name="user_list"), + path("/", user_detail, name="user_detail") +]) \ No newline at end of file