Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login API 구현 #26

Merged
merged 2 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions common/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import random
import string

from django.utils import timezone
from rest_framework.request import Request

Expand Down Expand Up @@ -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))
18 changes: 18 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pathlib import Path
import environ
import os
from datetime import timedelta


BASE_DIR = Path(__file__).resolve().parent.parent

Expand Down Expand Up @@ -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",
}
41 changes: 36 additions & 5 deletions users/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import random
import string

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


Expand All @@ -26,7 +25,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

Expand Down Expand Up @@ -54,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
7 changes: 4 additions & 3 deletions users/tests/views/test_confirm_user_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
71 changes: 71 additions & 0 deletions users/tests/views/test_login_view.py
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 8 additions & 2 deletions users/urls.py
Original file line number Diff line number Diff line change
@@ -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"),
]
39 changes: 32 additions & 7 deletions users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from users.serializers import (
UserConfirmCodeSerializer,
UserConfirmSerializer,
UserLoginSerializer,
UserSerializer,
)

Expand All @@ -33,9 +34,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
Expand All @@ -56,18 +57,42 @@ def post(self, request: Request) -> Response:
Args:
username: 이름
password: 비밀번호
code: 인증 코드
Returns:
username: 이름
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
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)