Skip to content

Commit 9d96634

Browse files
committed
Merge branch 'devel'
2 parents a834592 + b6b6a3c commit 9d96634

File tree

8 files changed

+210
-46
lines changed

8 files changed

+210
-46
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from rest_framework import permissions
2+
3+
4+
class IsOwnerPermission(permissions.BasePermission):
5+
6+
def has_permission(self, request, view):
7+
return request.user and request.user.is_authenticated
8+
9+
def has_object_permission(self, request, view, obj):
10+
11+
return (
12+
request.user.is_superuser or
13+
request.user.has_perm("accounts.view_user") or
14+
obj.pk == request.user.pk
15+
)
16+
17+
def add_or_change_permission_decorator(func):
18+
def wrapper(self, request, *args, **kwargs):
19+
user_id = kwargs.get('user_id')
20+
user_id = str(request.user.pk) if user_id == 'my' else user_id
21+
if request.method in ['POST', 'PUT', 'PATCH']:
22+
if not (
23+
request.user.is_superuser or
24+
(request.user.has_perm("accounts.change_user") and request.user.has_perm("accounts.add_user")) or
25+
str(request.user.pk) == user_id
26+
):
27+
return self.permission_denied(
28+
request,
29+
message="You do not have permission to perform this action."
30+
)
31+
return func(self, request, *args, **kwargs)
32+
return wrapper
33+
34+
def delete_permission_decorator(func):
35+
def wrapper(self, request, *args, **kwargs):
36+
user_id = kwargs.get('user_id')
37+
user_id = str(request.user.pk) if user_id == 'my' else user_id
38+
if not (
39+
request.user.is_superuser or
40+
request.user.has_perm("accounts.delete_user") or
41+
str(request.user.pk) == user_id
42+
):
43+
return self.permission_denied(
44+
request,
45+
message="You do not have permission to perform this action."
46+
)
47+
return func(self, request, *args, **kwargs)
48+
return wrapper

izpitnik/accounts/api/views.py

Lines changed: 69 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
11
# views.py
22
from typing import Literal
33

4+
from django.contrib.auth.mixins import UserPassesTestMixin
5+
from django.contrib.auth.models import update_last_login
46
from rest_framework import status
57
from rest_framework.exceptions import AuthenticationFailed
6-
from rest_framework.permissions import IsAuthenticated
8+
from rest_framework.generics import RetrieveUpdateDestroyAPIView
9+
from rest_framework.permissions import IsAuthenticated, AllowAny
710
from rest_framework.views import APIView
811
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
912
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
1013
from rest_framework_simplejwt.tokens import RefreshToken
1114
from rest_framework.response import Response
1215
from django.conf import settings
1316

17+
from izpitnik.accounts.api.permissions import IsOwnerPermission, add_or_change_permission_decorator, \
18+
delete_permission_decorator
19+
from izpitnik.accounts.mixins import GenerateTokenMixin
20+
from izpitnik.accounts.serializers import CustomTokenObtainPairSerializer, UserProfileSerializer
21+
from izpitnik.articles.api.permissions import IsAuthorOnAllMethodsPermission
1422
from izpitnik.settings import ENV
1523

1624

17-
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
1825

19-
@classmethod
20-
def get_token(cls, user):
21-
token = super().get_token(user)
22-
23-
token['is_admin'] = user.is_staff
24-
token['roles'] = [role.name for role in user.groups.all()]
25-
token['is_superuser'] = user.is_superuser
26-
27-
return token
2826

2927
class CustomTokenObtainPairView(TokenObtainPairView):
3028

@@ -33,25 +31,7 @@ class CustomTokenObtainPairView(TokenObtainPairView):
3331
def post(self, request, *args, **kwargs):
3432
response = super().post(request, *args, **kwargs)
3533
refresh = response.data.pop('refresh', None)
36-
37-
secure = False
38-
samesite = False
39-
40-
if ENV == "production":
41-
secure = True
42-
samesite = 'Strict'
43-
44-
if refresh:
45-
response.set_cookie(
46-
key='refresh_token',
47-
value=refresh,
48-
httponly=True,
49-
secure=secure,
50-
samesite=samesite,
51-
max_age=7 * 24 * 60 * 60, # 7 days
52-
path='/api/token',
53-
domain=request.build_absolute_uri('/')[:-1].replace("http://","").replace("https://","").split(":")[0]
54-
)
34+
response = GenerateTokenMixin(response=response, refresh=refresh).issue_token(request)
5535
return response
5636

5737
# views.py
@@ -77,28 +57,72 @@ class ApiLogoutView(APIView):
7757

7858
def post(self, request):
7959

80-
secure = False
81-
samesite = False
82-
83-
if ENV == "production":
84-
secure = True
85-
samesite = "Strict"
86-
8760
try:
88-
print(request.COOKIES.get("refresh_token"))
8961
refresh_token = request.COOKIES.get("refresh_token")
9062
token = RefreshToken(refresh_token)
9163
token.blacklist()
9264
except Exception as e:
9365
return Response({"error": str(e)}, status=400)
9466

95-
response = Response({"detail": "Logout successful."}, status=200)
96-
response.delete_cookie(
97-
key='refresh_token',
98-
samesite=samesite,
99-
path='/api/token',
100-
domain=request.build_absolute_uri('/')[:-1].replace("http://", "").replace("https://", "").split(":")[0]
101-
)
67+
return GenerateTokenMixin().unset_cookie(request)
68+
69+
class ApiSignUpVew(UserPassesTestMixin,APIView):
70+
71+
permission_classes = [AllowAny]
72+
73+
def test_func(self):
74+
return not self.request.user.is_authenticated
75+
76+
def post(self, request):
77+
78+
from izpitnik.accounts.forms import MainUserCreationForm
79+
80+
data = request.data
81+
82+
form = MainUserCreationForm(data)
83+
84+
if form.is_valid():
85+
user = form.save()
86+
token = CustomTokenObtainPairSerializer.get_token(user)
87+
return GenerateTokenMixin(refresh=str(token), access=str(token.access_token)).issue_token(request)
88+
89+
else:
90+
return Response(form.errors, status=400)
91+
92+
class GetUpdateDeleteProfileAPIView(RetrieveUpdateDestroyAPIView):
93+
permission_classes = [IsOwnerPermission]
94+
lookup_url_kwarg = "user_id"
95+
serializer_class = UserProfileSerializer
96+
97+
def get_queryset(self):
98+
from izpitnik.accounts.models import User
99+
user_id = self.rectify_kwarg(self.request, self.kwargs).get(self.lookup_url_kwarg)
100+
return User.objects.filter(pk=user_id).prefetch_related("profile")
101+
102+
def get(self, request, *args, **kwargs):
103+
kwargs = self.rectify_kwarg(request, kwargs)
104+
response = super().get(request,*args, **kwargs)
102105
return response
103106

107+
def rectify_kwarg(self, request, kwargs):
108+
user_id = kwargs.get(self.lookup_url_kwarg)
109+
if user_id == 'my':
110+
kwargs[self.lookup_url_kwarg] = str(request.user.pk)
111+
return kwargs
112+
113+
114+
@add_or_change_permission_decorator
115+
def put(self, request, *args, **kwargs):
116+
kwargs = self.rectify_kwarg(request, kwargs)
117+
return super().put(request, *args, **kwargs)
118+
119+
@add_or_change_permission_decorator
120+
def patch(self, request, *args, **kwargs):
121+
kwargs = self.rectify_kwarg(request, kwargs)
122+
return super().patch(request, *args, **kwargs)
123+
124+
@delete_permission_decorator
125+
def delete(self, request, *args, **kwargs):
126+
kwargs = self.rectify_kwarg(request, kwargs)
127+
return super().delete(request, *args, **kwargs)
104128

izpitnik/accounts/mixins.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from rest_framework.response import Response
2+
from izpitnik.settings import ENV
3+
4+
5+
class GenerateTokenMixin():
6+
7+
def __init__(self, *args, **kwargs):
8+
self.response = kwargs.get('response', args[0] if args else None)
9+
self.refresh = kwargs.get('refresh', args[1] if len(args) > 1 else None)
10+
self.access = kwargs.get('access', args[2] if len(args) > 2 else None)
11+
self.secure = True if ENV == "production" else False
12+
self.same_site = 'Strict' if ENV == "production" else False
13+
14+
def issue_token(self, request):
15+
response = self.response or Response({"access":self.access},status=201)
16+
if self.refresh:
17+
response.set_cookie(
18+
key='refresh_token',
19+
value=self.refresh,
20+
httponly=True,
21+
secure=self.secure,
22+
samesite=self.same_site,
23+
max_age=7 * 24 * 60 * 60, # 7 days
24+
path='/api/token',
25+
domain=self.build_domain(request)
26+
)
27+
return response
28+
29+
def unset_cookie(self, request):
30+
31+
response = Response({"detail": "Logout successful."}, status=200)
32+
response.delete_cookie(
33+
key='refresh_token',
34+
samesite=self.same_site,
35+
path='/api/token',
36+
domain=self.build_domain(request)
37+
)
38+
return response
39+
40+
@staticmethod
41+
def build_domain(request):
42+
return request.build_absolute_uri('/')[:-1].replace("http://", "").replace("https://", "").split(":")[0]

izpitnik/accounts/serializers.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from rest_framework import serializers
2+
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
3+
4+
from izpitnik.accounts.models import User, Profile
5+
6+
7+
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
8+
9+
@classmethod
10+
def get_token(cls, user):
11+
token = super().get_token(user)
12+
13+
token['is_admin'] = user.is_staff
14+
token['roles'] = [role.name for role in user.groups.all()]
15+
token['is_superuser'] = user.is_superuser
16+
17+
return token
18+
19+
class ProfileSerializer(serializers.ModelSerializer):
20+
class Meta:
21+
model = Profile
22+
fields = ['description', 'image', 'birth_date']
23+
24+
class UserProfileSerializer(serializers.ModelSerializer):
25+
26+
profile = ProfileSerializer(required=False)
27+
28+
class Meta:
29+
model = User
30+
depth = 1
31+
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'date_joined', 'profile']
32+
33+
def update(self, instance, validated_data):
34+
profile_data = validated_data.pop('profile', {})
35+
profile = instance.profile
36+
37+
for attr, value in validated_data.items():
38+
setattr(instance, attr, value)
39+
instance.save()
40+
41+
if profile_data is not None:
42+
for attr, value in profile_data.items():
43+
setattr(profile, attr, value)
44+
profile.save()
45+
46+
return instance

izpitnik/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
'SLIDING_TOKEN_LIFETIME': timedelta(days=30),
9999
'SLIDING_TOKEN_REFRESH_LIFETIME_LATE_USER': timedelta(days=1),
100100
'SLIDING_TOKEN_LIFETIME_LATE_USER': timedelta(days=30),
101+
'UPDATE_LAST_LOGIN': True,
101102
}
102103

103104
SPECTACULAR_SETTINGS = {

izpitnik/urls.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@
2424
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
2525

2626
from izpitnik import settings
27-
from izpitnik.accounts.api.views import CustomTokenObtainPairView, CookieTokenRefreshView, ApiLogoutView
27+
from izpitnik.accounts.api.views import CustomTokenObtainPairView, CookieTokenRefreshView, ApiLogoutView, ApiSignUpVew, \
28+
GetUpdateDeleteProfileAPIView
2829
from izpitnik.articles.api.views import ArtilceAPIView, CreateArticleAPIView, GetUpdateDeleteArticleAPIView
2930

3031
urlpatterns = [
3132
path('admin/', admin.site.urls),
3233
path('', include('izpitnik.common.urls')),
3334
path('api/', include([
3435
path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
36+
path( 'register/', ApiSignUpVew.as_view(), name='api_register'),
37+
path( 'profile/<slug:user_id>/', GetUpdateDeleteProfileAPIView.as_view(), name='api_profile'),
3538
path('token/refresh/', CookieTokenRefreshView.as_view(), name='token_refresh'),
3639
path('token/logout/', ApiLogoutView.as_view(), name='logout-api'),
3740
path('articles/', ArtilceAPIView.as_view(), name='articles-api'),
3.01 MB
Loading
3.12 MB
Loading

0 commit comments

Comments
 (0)