Skip to content

Commit

Permalink
Merge pull request #26 from Django-Wanted-Internship-3-Team/feature/i…
Browse files Browse the repository at this point in the history
…ssue-008

Login API 구현
  • Loading branch information
lfoyh6591 authored Oct 31, 2023
2 parents 13a0b91 + 9738e8d commit d16aa7d
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 17 deletions.
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)

0 comments on commit d16aa7d

Please sign in to comment.