Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f8fc12a

Browse files
Add login/logout API (#104)
* add login/logout API * review fixes * review fixes * review fixes * review fixes
1 parent 29e629f commit f8fc12a

14 files changed

+307
-92
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,14 @@ More at:
277277
https://docs.astral.sh/ruff/linter/
278278
https://docs.astral.sh/ruff/formatter/
279279

280+
### Removing Expired Tokens in Django
281+
282+
To remove old, expired tokens that have been blacklisted, use the following management command:
283+
284+
```bash
285+
python manage.py flushexpiredtokens
286+
```
287+
280288
### Usage with Docker 🐳
281289
For setup and running with Docker, refer to the [Docker configuration instructions](DOCKER.md).
282290

conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pytest
66
from django.contrib.auth import get_user_model
7+
from django.urls import reverse
78

89
from core.models import Community, CommunityMember, Post
910

@@ -168,6 +169,11 @@ def _create_communties(count: int, posts_per_community: int, privacy: str = Comm
168169
return _create_communties
169170

170171

172+
@pytest.fixture()
173+
def api_register_url() -> str:
174+
return reverse("api-user-registration")
175+
176+
171177
@pytest.fixture()
172178
def post(user: User, community: Community) -> Generator[Post, None, None]:
173179
return Post.objects.create(

poetry.lock

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ djangorestframework = "^3.15.2"
2323
freezegun = "^1.5.1"
2424
pytest-xdist = "^3.6.1"
2525
social-auth-app-django = "5.4.1"
26+
djangorestframework-simplejwt = "^5.3.1"
2627
drf-yasg = "^1.21.8"
2728

2829
[tool.poetry.group.dev.dependencies]

reddit/api_documentation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
license=openapi.License(name="BSD License"),
1717
),
1818
public=True,
19-
permission_classes=(permissions.IsAuthenticated,),
19+
permission_classes=(permissions.AllowAny,),
2020
)
2121

2222

reddit/settings.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"""
1111

1212
import json
13+
from datetime import timedelta
1314
from pathlib import Path
1415

1516
from decouple import config
@@ -44,6 +45,8 @@
4445
"django.contrib.messages",
4546
"django.contrib.staticfiles",
4647
"rest_framework",
48+
"rest_framework_simplejwt",
49+
"rest_framework_simplejwt.token_blacklist",
4750
"crispy_forms",
4851
"crispy_bootstrap5",
4952
"django_timesince",
@@ -119,6 +122,9 @@
119122
]
120123
REST_FRAMEWORK = {
121124
"DEFAULT_SCHEMA_CLASS": "drf_yasg.openapi.AutoSchema",
125+
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
126+
"PAGE_SIZE": 10,
127+
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTStatelessUserAuthentication",),
122128
}
123129

124130

@@ -164,6 +170,13 @@
164170

165171
LAST_ACTIVITY_ONLINE_LIMIT_MINUTES = 15
166172

173+
174+
SIMPLE_JWT = {
175+
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
176+
"BLACKLIST_AFTER_ROTATION": True,
177+
"ROTATE_REFRESH_TOKENS": True,
178+
}
179+
167180
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
168181
EMAIL_HOST = config("EMAIL_HOST", default="smtp.gmail.com")
169182
EMAIL_PORT = config("EMAIL_PORT", default=587)
@@ -182,7 +195,6 @@
182195
LIMIT_WARNINGS = 5
183196
LOGIN_URL = reverse_lazy("login")
184197
LOGIN_REDIRECT_URL = "/"
185-
REST_FRAMEWORK = {"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10}
186198
DEFAULT_AVATAR_URL = "/media/users_avatars/default.png"
187199
DEFAULT_BANNER_URL = "/media/users_banners/default_banner.jpg"
188200

reddit/tests/test_api_documentation.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,6 @@ def test_swagger_view_user_authenticated(client: Client, user: User) -> None:
1717
assert response.status_code == 405
1818

1919

20-
@pytest.mark.django_db()
21-
def test_swagger_view_user_non_authenticated(client: Client, user: User) -> None:
22-
response = client.get(reverse_lazy("schema-swagger-ui"))
23-
assert response.status_code == 403
24-
response = client.post(reverse_lazy("schema-swagger-ui"))
25-
assert response.status_code == 405
26-
27-
2820
@pytest.mark.django_db()
2921
def test_redoc_view_user_authenticated(client: Client, user: User) -> None:
3022
data = {"username": user.email, "password": user.plain_password}
@@ -34,11 +26,3 @@ def test_redoc_view_user_authenticated(client: Client, user: User) -> None:
3426
assert response.status_code == 200
3527
response = client.post(reverse_lazy("schema-redoc"))
3628
assert response.status_code == 405
37-
38-
39-
@pytest.mark.django_db()
40-
def test_redoc_view_user_non_authenticated(client: Client, user: User) -> None:
41-
response = client.get(reverse_lazy("schema-redoc"))
42-
assert response.status_code == 403
43-
response = client.post(reverse_lazy("schema-redoc"))
44-
assert response.status_code == 405

users/api_urls.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from rest_framework.urls import path
2+
from rest_framework_simplejwt.views import (
3+
TokenRefreshView,
4+
)
25

3-
from .api_views import UserAPIRegistration
6+
from .api_views import UserAPILogin, UserAPILogout, UserAPIRegistration
47

58
urlpatterns = [
69
path("register/", UserAPIRegistration.as_view(), name="api-user-registration"),
10+
path("login/", UserAPILogin.as_view(), name="api-user-login"),
11+
path("logout/", UserAPILogout.as_view(), name="api-user-logout"),
12+
path("token-refresh/", TokenRefreshView.as_view(), name="api-token-refresh"),
713
]

users/api_views.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1+
from typing import Any, ClassVar
2+
3+
from django.contrib.auth import authenticate, login, logout
4+
from requests import Request
5+
from rest_framework import status
16
from rest_framework.generics import CreateAPIView
7+
from rest_framework.permissions import IsAuthenticated
8+
from rest_framework.response import Response
9+
from rest_framework_simplejwt.authentication import JWTStatelessUserAuthentication
10+
from rest_framework_simplejwt.exceptions import TokenError
11+
from rest_framework_simplejwt.tokens import RefreshToken
12+
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
213

314
from .models import User
415
from .serializers import UserSerializer
@@ -7,3 +18,26 @@
718
class UserAPIRegistration(CreateAPIView):
819
queryset = User.objects.all()
920
serializer_class = UserSerializer
21+
22+
23+
class UserAPILogin(TokenObtainPairView):
24+
def post(self: "UserAPILogin", request: Request, *args: tuple, **kwargs: dict[str, Any]) -> Response:
25+
response = super().post(request, *args, **kwargs)
26+
user = authenticate(username=self.request.data["email"], password=self.request.data["password"])
27+
login(request, user)
28+
return response
29+
30+
31+
class UserAPILogout(TokenRefreshView):
32+
permission_classes: ClassVar[list] = [IsAuthenticated]
33+
authentication_classes: ClassVar[list] = [JWTStatelessUserAuthentication]
34+
35+
def post(self: "UserAPILogout", request: Request, **kwargs: dict[str, Any]) -> Response: # noqa: ARG002
36+
try:
37+
refresh_token = request.data["refresh"]
38+
token = RefreshToken(refresh_token)
39+
token.blacklist()
40+
logout(request)
41+
return Response(data={"message": "Logged out successfully"}, status=status.HTTP_202_ACCEPTED)
42+
except TokenError as e:
43+
return Response(data={"message": str(e)}, status=status.HTTP_400_BAD_REQUEST)

users/serializers.py

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
import typing
22

3-
from django.contrib.sites.shortcuts import get_current_site
4-
from django.core.mail import send_mail
53
from django.db.models import Model
6-
from django.template.loader import render_to_string
7-
from django.urls import reverse
8-
from django.utils.encoding import force_bytes
9-
from django.utils.http import urlsafe_base64_encode
104
from rest_framework import serializers
115

12-
from reddit import settings
13-
146
from .models import User
15-
from .tokens import account_activation_token
7+
from .utils import user_creation
168

179

1810
class UserSerializer(serializers.ModelSerializer):
@@ -34,27 +26,8 @@ def create(self: "UserSerializer", validated_data: dict) -> User:
3426
user = User.objects.create(nickname=validated_data["nickname"], email=validated_data["email"], is_active=False)
3527
user.set_password(validated_data["password"])
3628
user.save()
37-
uid = urlsafe_base64_encode(force_bytes(user.pk))
38-
token = account_activation_token.make_token(user)
3929
request = self.context.get("request")
40-
protocol = "https" if request.is_secure() else "http"
41-
current_site = get_current_site(request)
42-
activation_link = reverse("activate-account", kwargs={"uidb64": uid, "token": token})
43-
full_activation_link = f"{protocol}://{current_site.domain}{activation_link}"
44-
send_mail(
45-
"Confirm your registration",
46-
f"Please click on the following link to confirm your registration " f"{activation_link}",
47-
settings.EMAIL_HOST_USER,
48-
[user.email],
49-
fail_silently=False,
50-
html_message=render_to_string(
51-
"users/account_activation_email.html",
52-
{
53-
"user": user,
54-
"activation_link": full_activation_link,
55-
},
56-
),
57-
)
30+
user_creation(user, request)
5831
return user
5932

6033
def get_message(self: "UserSerializer", obj: User) -> str:

0 commit comments

Comments
 (0)