From 552caf01a11c717bc4beeb76fceb49402e5a3fdb Mon Sep 17 00:00:00 2001 From: seongwon Date: Mon, 30 Oct 2023 16:29:48 +0900 Subject: [PATCH 1/2] =?UTF-8?q?:recycle:Refact=20:=20PR=20comment=EC=97=90?= =?UTF-8?q?=20=EB=94=B0=EB=A5=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/utils.py | 7 +++++++ users/serializers.py | 6 ++---- users/views.py | 12 ++++++------ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/common/utils.py b/common/utils.py index 92ffe28..11a61c9 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,3 +1,6 @@ +import random +import string + from django.utils import timezone from rest_framework.request import Request @@ -55,3 +58,7 @@ def get_now() -> timezone: def get_before_week() -> timezone: return (timezone.now() - timezone.timedelta(days=7)).strftime("%Y-%m-%d") + + +def get_random_string(length=6) -> string: + return "".join(random.choice(string.ascii_letters + string.digits) for i in range(length)) diff --git a/users/serializers.py b/users/serializers.py index d7bef42..f664824 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -1,9 +1,7 @@ -import random -import string - from django.contrib.auth import get_user_model, password_validation from rest_framework import serializers +from common.utils import get_random_string from users.models import UserConfirmCode @@ -26,7 +24,7 @@ class UserConfirmCodeSerializer(UserSerializer): def create(self, validated_data): user = super().create(validated_data) - confirm_code = "".join(random.choice(string.ascii_letters + string.digits) for i in range(6)) + confirm_code = get_random_string() user_confirm_code = UserConfirmCode.objects.create(code=confirm_code, user=user) return user_confirm_code diff --git a/users/views.py b/users/views.py index 46c04ec..70c9852 100644 --- a/users/views.py +++ b/users/views.py @@ -33,9 +33,9 @@ def post(self, request: Request) -> Response: username: 생성된 계정 이름 code: 생성된 인증 코드 """ - user_confirm_code_serializer = UserConfirmCodeSerializer(data=request.data) - user_confirm_code_serializer.is_valid(raise_exception=True) - user_confirm_code = user_confirm_code_serializer.save() + serializer = UserConfirmCodeSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user_confirm_code = serializer.save() response_data = UserSerializer(user_confirm_code.user).data response_data["confirm_code"] = user_confirm_code.code @@ -62,9 +62,9 @@ def post(self, request: Request) -> Response: is_confirmed: 인증 여부 """ user = get_object_or_404(get_user_model(), username=request.data["username"]) - user_confirm_serializer = UserConfirmSerializer(user, data=request.data) - user_confirm_serializer.is_valid(raise_exception=True) - user_confirm_serializer.save() + serializer = UserConfirmSerializer(user, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() response_data = {} response_data["username"] = user.username From 9738e8de683da6154d839055701d2b890c5330b2 Mon Sep 17 00:00:00 2001 From: seongwon Date: Mon, 30 Oct 2023 18:15:33 +0900 Subject: [PATCH 2/2] =?UTF-8?q?:sparkles:Feat=20:=20Login=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20confirm=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20-=20login=20serial?= =?UTF-8?q?izer,=20view,=20url=20=EA=B5=AC=ED=98=84=20-=20token=20refresh?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20-=20JWT=20setting=20-=20user=20confirm?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 18 ++++++ users/serializers.py | 35 +++++++++- users/tests/views/test_confirm_user_view.py | 7 +- users/tests/views/test_login_view.py | 71 +++++++++++++++++++++ users/urls.py | 10 ++- users/views.py | 27 +++++++- 6 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 users/tests/views/test_login_view.py diff --git a/config/settings.py b/config/settings.py index 0970411..7f64160 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,6 +1,8 @@ from pathlib import Path import environ import os +from datetime import timedelta + BASE_DIR = Path(__file__).resolve().parent.parent @@ -185,3 +187,19 @@ } }, } + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30), + "REFRESH_TOKEN_LIFETIME": timedelta(days=7), + "ROTATE_REFRESH_TOKENS": False, + "BLACKLIST_AFTER_ROTATION": True, + "ALGORITHM": "HS256", + "SIGNING_KEY": SECRET_KEY, + "VERIFYING_KEY": None, + "AUTH_HEADER_TYPES": ("Bearer",), + "USER_ID_FIELD": "id", + "USER_ID_CLAIM": "user_id", + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), + "TOKEN_TYPE_CLAIM": "token_type", + "JTI_CLAIM": "jti", +} diff --git a/users/serializers.py b/users/serializers.py index f664824..3bfeee1 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -1,5 +1,6 @@ -from django.contrib.auth import get_user_model, password_validation +from django.contrib.auth import authenticate, get_user_model, password_validation from rest_framework import serializers +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from common.utils import get_random_string from users.models import UserConfirmCode @@ -52,3 +53,35 @@ def update(self, user, validated_data): user.is_confirmed = True user.save() return user + + +class UserLoginSerializer(serializers.Serializer): + username = serializers.CharField(required=True) + password = serializers.CharField(required=True, write_only=True) + token = serializers.SerializerMethodField(read_only=True) + + def get_token(self, user): + if user is not None: + refresh = TokenObtainPairSerializer.get_token(user) + refresh["username"] = user.username + data = { + "refresh": str(refresh), + "access": str(refresh.access_token), + } + return data + return None + + def validate(self, data): + user = authenticate(username=data["username"], password=data["password"]) + if user is None: + raise serializers.ValidationError("Username or Password is Incorrect") + if not user.is_confirmed: + raise serializers.ValidationError("User is not confirmed yet") + return data + + def create(self, validated_data): + user = authenticate(username=validated_data["username"], password=validated_data["password"]) + if user is not None: + user.is_active = True + user.save() + return user diff --git a/users/tests/views/test_confirm_user_view.py b/users/tests/views/test_confirm_user_view.py index 251ffa8..27ace61 100644 --- a/users/tests/views/test_confirm_user_view.py +++ b/users/tests/views/test_confirm_user_view.py @@ -51,7 +51,7 @@ def test_post_signup_fail_user_not_found(self): def test_post_signup_fail_invalid_password(self): response = self.client.post( - path=reverse("signup"), + path=reverse("confirm"), data=json.dumps( { "username": "testusername1", @@ -65,7 +65,7 @@ def test_post_signup_fail_invalid_password(self): def test_post_signup_fail_invalid_code(self): response = self.client.post( - path=reverse("signup"), + path=reverse("confirm"), data=json.dumps( { "username": "testusername1", @@ -79,8 +79,9 @@ def test_post_signup_fail_invalid_code(self): def test_post_signup_fail_already_confirmed(self): self.user.is_confirmed = True + self.user.save() response = self.client.post( - path=reverse("signup"), + path=reverse("confirm"), data=json.dumps( { "username": "testusername1", diff --git a/users/tests/views/test_login_view.py b/users/tests/views/test_login_view.py new file mode 100644 index 0000000..e046610 --- /dev/null +++ b/users/tests/views/test_login_view.py @@ -0,0 +1,71 @@ +import json + +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + +from users.models import User + + +class SignupViewTest(APITestCase): + @classmethod + def setUpTestData(cls): + cls.confirmed_user = User.objects.create_user( + email="testuser1@example.com", username="testusername1", password="testpassword", is_confirmed=True + ) + cls.not_confirmed_user = User.objects.create_user( + email="testuser2@example.com", username="testusername2", password="testpassword", is_confirmed=False + ) + + def test_post_login_success(self): + response = self.client.post( + path=reverse("login"), + data=json.dumps( + { + "username": "testusername1", + "password": "testpassword", + } + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.confirmed_user.is_active, True) + + def test_post_login_fail_user_not_found(self): + response = self.client.post( + path=reverse("login"), + data=json.dumps( + { + "username": "testusername3", + "password": "testpassword", + } + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_post_login_fail_invalid_password(self): + response = self.client.post( + path=reverse("login"), + data=json.dumps( + { + "username": "testusername1", + "password": "testpassword2", + } + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_post_login_fail_not_confirmed_user(self): + response = self.client.post( + path=reverse("login"), + data=json.dumps( + { + "username": "testusername2", + "password": "testpassword", + } + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/users/urls.py b/users/urls.py index 4b70f0f..8e6b4ef 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,5 +1,11 @@ from django.urls import path +from rest_framework_simplejwt.views import TokenRefreshView -from users.views import ConfirmUserView, SignupView +from users.views import ConfirmUserView, LoginView, SignupView -urlpatterns = [path("signup/", SignupView.as_view(), name="signup"), path("confirm/", ConfirmUserView.as_view(), name="confirm")] +urlpatterns = [ + path("signup/", SignupView.as_view(), name="signup"), + path("confirm/", ConfirmUserView.as_view(), name="confirm"), + path("login/", LoginView.as_view(), name="login"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), +] diff --git a/users/views.py b/users/views.py index 70c9852..f84fc67 100644 --- a/users/views.py +++ b/users/views.py @@ -9,6 +9,7 @@ from users.serializers import ( UserConfirmCodeSerializer, UserConfirmSerializer, + UserLoginSerializer, UserSerializer, ) @@ -56,7 +57,6 @@ def post(self, request: Request) -> Response: Args: username: 이름 password: 비밀번호 - code: 인증 코드 Returns: username: 이름 is_confirmed: 인증 여부 @@ -71,3 +71,28 @@ def post(self, request: Request) -> Response: response_data["is_confirmed"] = user.is_confirmed return Response(response_data, status=status.HTTP_200_OK) + + +class LoginView(APIView): + @swagger_auto_schema( + operation_summary="유저 로그인", + request_body=UserLoginSerializer, + responses={ + status.HTTP_200_OK: UserLoginSerializer, + }, + ) + def post(self, request: Request) -> Response: + """ + username, paswword를 받아 유저 계정을 활성화하고 JWT 토큰을 발급합니다. + Args: + username: 이름 + password: 비밀번호 + Returns: + username: 이름 + token: access token과 refresh token + """ + serializer = UserLoginSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data, status=status.HTTP_200_OK)