create authentication app to separate authentication from business api

sync
Laurent 8 months ago
parent 6b97eaf850
commit 57ea6e8e78
  1. 33
      api/serializers.py
  2. 8
      api/urls.py
  3. 6
      api/utils.py
  4. 100
      api/views.py
  5. 0
      authentication/__init__.py
  6. 15
      authentication/admin.py
  7. 6
      authentication/apps.py
  8. 36
      authentication/migrations/0001_initial.py
  9. 18
      authentication/migrations/0002_rename_model_name_device_device_model.py
  10. 0
      authentication/migrations/__init__.py
  11. 2
      authentication/models/__init__.py
  12. 3
      authentication/models/device.py
  13. 13
      authentication/models/login_log.py
  14. 29
      authentication/serializers.py
  15. 3
      authentication/tests.py
  16. 5
      authentication/utils.py
  17. 109
      authentication/views.py
  18. 1
      padelclub_backend/settings.py
  19. 9
      sync/admin.py
  20. 16
      sync/migrations/0004_delete_device.py
  21. 1
      sync/models/__init__.py
  22. 4
      sync/signals.py

@ -1,8 +1,6 @@
from rest_framework import serializers
from tournaments.models.court import Court
from tournaments.models import Club, LiveMatch, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, DateInterval, Log, DeviceToken, UnregisteredTeam, UnregisteredPlayer
from django.contrib.auth import password_validation
from django.utils.translation import gettext_lazy as _
from django.db.utils import IntegrityError
from django.conf import settings
@ -14,7 +12,6 @@ from django.core.mail import EmailMessage
from django.contrib.sites.shortcuts import get_current_site
from api.tokens import account_activation_token
# from tournaments.models.data_access import DataAccess
from shared.cryptography import encryption_util
from tournaments.models.draw_log import DrawLog
@ -146,7 +143,7 @@ class RoundSerializer(serializers.ModelSerializer):
class GroupStageSerializer(serializers.ModelSerializer):
class Meta:
model = GroupStage
fields = '__all__' # ['id', 'index', 'tournament_id', 'format']
fields = '__all__'
class MatchSerializer(serializers.ModelSerializer):
class Meta:
@ -156,7 +153,7 @@ class MatchSerializer(serializers.ModelSerializer):
class TeamScoreSerializer(serializers.ModelSerializer):
class Meta:
model = TeamScore
fields = '__all__' # ['id', 'match_id', 'score', 'walk_out', 'lucky_loser', 'player_registrations']
fields = '__all__'
class TeamRegistrationSerializer(serializers.ModelSerializer):
class Meta:
@ -180,31 +177,6 @@ class PurchaseSerializer(serializers.ModelSerializer):
validated_data['user'] = user
return super().create(validated_data)
class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(max_length=128, write_only=True, required=True)
new_password1 = serializers.CharField(max_length=128, write_only=True, required=True)
new_password2 = serializers.CharField(max_length=128, write_only=True, required=True)
def validate_old_password(self, value):
user = self.context['request'].user
if not user.check_password(value):
raise serializers.ValidationError(
_('Your old password was entered incorrectly. Please enter it again.')
)
return value
def validate(self, data):
if data['new_password1'] != data['new_password2']:
raise serializers.ValidationError({'new_password2': _("The two password fields didn't match.")})
password_validation.validate_password(data['new_password1'], self.context['request'].user)
return data
def save(self, **kwargs):
password = self.validated_data['new_password1']
user = self.context['request'].user
user.set_password(password)
user.save()
return user
class LiveMatchSerializer(serializers.ModelSerializer):
class Meta:
@ -237,7 +209,6 @@ class DeviceTokenSerializer(serializers.ModelSerializer):
fields = '__all__'
read_only_fields = ['user']
class DrawLogSerializer(serializers.ModelSerializer):
class Meta:
model = DrawLog

@ -4,6 +4,7 @@ from rest_framework.authtoken.views import obtain_auth_token
from . import views
from sync.views import SynchronizationApi, UserDataAccessApi, DataAccessViewSet
from authentication.views import CustomAuthToken, Logout, ChangePasswordView
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
@ -36,10 +37,11 @@ urlpatterns = [
path('api-token-auth/', obtain_auth_token, name='api_token_auth'),
path("user-by-token/", views.user_by_token, name="user_by_token"),
path("change-password/", views.ChangePasswordView.as_view(), name="change_password"),
path('token-auth/', views.CustomAuthToken.as_view()),
path('api-token-logout/', views.Logout.as_view()),
# authentication
path("change-password/", ChangePasswordView.as_view(), name="change_password"),
path('token-auth/', CustomAuthToken.as_view()),
path('api-token-logout/', Logout.as_view()),
# forgotten password
path('dj-rest-auth/', include('dj_rest_auth.urls')),

@ -1,9 +1,3 @@
import re
def is_valid_email(email):
email_regex = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
return re.match(email_regex, email) is not None
def check_version_smaller_than_1_1_12(version_str):
# Remove the parentheses part if it exists, example of version: 1.1.12 (2)
version_str = version_str.split()[0]

@ -1,102 +1,20 @@
from pandas.io.feather_format import pd
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer, UnregisteredTeamSerializer, UnregisteredPlayerSerializer
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer, UnregisteredTeamSerializer, UnregisteredPlayerSerializer
from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer
from rest_framework import viewsets, permissions
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status
from rest_framework.generics import UpdateAPIView
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from django.http import Http404
from django.contrib.auth import authenticate
from django.db.models import Q
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from django.apps import apps
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from collections import defaultdict
from .permissions import IsClubOwner
from .utils import is_valid_email, check_version_smaller_than_1_1_12
from sync.models import Device
from .utils import check_version_smaller_than_1_1_12
from shared.discord import send_discord_log_message
@method_decorator(csrf_exempt, name='dispatch')
class CustomAuthToken(APIView):
permission_classes = []
def post(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')
device_id = request.data.get('device_id')
user = authenticate(username=username, password=password)
if user is None and is_valid_email(username) == True:
true_username = self.get_username_from_email(username)
user = authenticate(username=true_username, password=password)
if user:
# user.device_id = device_id
# user.save()
# self.create_or_update_device(user, device_id)
# token, created = Token.objects.get_or_create(user=user)
# return Response({'token': token.key})
if user.device_id is None or user.device_id == device_id or user.username == 'apple-test':
user.device_id = device_id
user.save()
self.create_or_update_device(user, device_id)
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
else:
return Response({'error': 'Vous ne pouvez pour l\'instant vous connecter sur plusieurs appareils en même temps. Veuillez vous déconnecter du précédent appareil. Autrement, veuillez contacter le support.'}, status=status.HTTP_403_FORBIDDEN)
else:
return Response({'error': 'L\'utilisateur et le mot de passe de correspondent pas'}, status=status.HTTP_401_UNAUTHORIZED)
def create_or_update_device(self, user, device_id):
Device.objects.update_or_create(
id=device_id,
defaults={
'user': user
}
)
def get_username_from_email(self, email):
try:
user = CustomUser.objects.get(email=email)
return user.username
except ObjectDoesNotExist:
return None
class Logout(APIView):
permission_classes = (IsAuthenticated,)
def post(self, request, *args, **kwargs):
# request.user.auth_token.delete()
device_id = request.data.get('device_id')
if request.user.device_id == device_id:
request.user.device_id = None
request.user.save()
Device.objects.filter(id=device_id).delete()
return Response(status=status.HTTP_200_OK)
@api_view(['GET'])
def user_by_token(request):
serializer = UserSerializer(request.user)
@ -174,20 +92,6 @@ class PurchaseViewSet(SoftDeleteViewSet):
def delete(self, request, pk):
raise MethodNotAllowed('DELETE')
class ChangePasswordView(UpdateAPIView):
serializer_class = ChangePasswordSerializer
def update(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
# if using drf authtoken, create a new token
if hasattr(user, 'auth_token'):
user.auth_token.delete()
token, created = Token.objects.get_or_create(user=user)
# return new token
return Response({'token': token.key}, status=status.HTTP_200_OK)
class EventViewSet(SoftDeleteViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer

@ -0,0 +1,15 @@
from django.contrib import admin
from .models import Device, LoginLog
class DeviceAdmin(admin.ModelAdmin):
list_display = ['user', 'device_model', 'last_login', 'id']
readonly_fields = ('last_login',)
ordering = ['-last_login']
class LoginLogAdmin(admin.ModelAdmin):
list_display = ['user', 'device', 'date']
ordering = ['-date']
admin.site.register(Device, DeviceAdmin)
admin.site.register(LoginLog, LoginLogAdmin)

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AuthenticationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'authentication'

@ -0,0 +1,36 @@
# Generated by Django 5.1 on 2025-03-20 14:49
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Device',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('last_login', models.DateTimeField(auto_now=True)),
('model_name', models.CharField(blank=True, max_length=100, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='devices', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='LoginLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('date', models.DateTimeField(auto_now=True)),
('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='login_logs', to='authentication.device')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_logs', to=settings.AUTH_USER_MODEL)),
],
),
]

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2025-03-20 15:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='device',
old_name='model_name',
new_name='device_model',
),
]

@ -0,0 +1,2 @@
from .device import Device
from .login_log import LoginLog

@ -6,6 +6,7 @@ class Device(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='devices')
last_login = models.DateTimeField(auto_now=True)
device_model = models.CharField(max_length=100, blank=True, null=True)
def __str__(self):
return f"{self.id} > {self.user.username}"
return f"{self.user.username} : {self.device_model}"

@ -0,0 +1,13 @@
from django.db import models
import uuid
from django.conf import settings
from . import Device
class LoginLog(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='login_logs')
device = models.ForeignKey(Device, on_delete=models.SET_NULL, related_name='login_logs', null=True)
date = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.id} > {self.user.username}"

@ -0,0 +1,29 @@
from django.contrib.auth import password_validation
from rest_framework import serializers
class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(max_length=128, write_only=True, required=True)
new_password1 = serializers.CharField(max_length=128, write_only=True, required=True)
new_password2 = serializers.CharField(max_length=128, write_only=True, required=True)
def validate_old_password(self, value):
user = self.context['request'].user
if not user.check_password(value):
raise serializers.ValidationError(
_('Your old password was entered incorrectly. Please enter it again.')
)
return value
def validate(self, data):
if data['new_password1'] != data['new_password2']:
raise serializers.ValidationError({'new_password2': _("The two password fields didn't match.")})
password_validation.validate_password(data['new_password1'], self.context['request'].user)
return data
def save(self, **kwargs):
password = self.validated_data['new_password1']
user = self.context['request'].user
user.set_password(password)
user.save()
return user

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,5 @@
import re
def is_valid_email(email):
email_regex = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
return re.match(email_regex, email) is not None

@ -0,0 +1,109 @@
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth import authenticate
from django.utils.decorators import method_decorator
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.authtoken.models import Token
from rest_framework import status
from rest_framework.generics import UpdateAPIView
from .utils import is_valid_email
from .models import Device, LoginLog
from .serializers import ChangePasswordSerializer
CustomUser=settings.AUTH_USER_MODEL
@method_decorator(csrf_exempt, name='dispatch')
class CustomAuthToken(APIView):
permission_classes = []
def post(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')
device_id = request.data.get('device_id')
user = authenticate(username=username, password=password)
if user is None and is_valid_email(username) == True:
true_username = self.get_username_from_email(username)
user = authenticate(username=true_username, password=password)
if user:
# user.device_id = device_id
# user.save()
# self.create_or_update_device(user, device_id)
# token, created = Token.objects.get_or_create(user=user)
# return Response({'token': token.key})
if user.device_id is None or user.device_id == device_id or user.username == 'apple-test':
user.device_id = device_id
user.save()
device_model = request.data.get('device_model')
device = self.create_or_update_device(user, device_id, device_model)
self.create_login_log(user, device)
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
else:
return Response({'error': 'Vous ne pouvez pour l\'instant vous connecter sur plusieurs appareils en même temps. Veuillez vous déconnecter du précédent appareil. Autrement, veuillez contacter le support.'}, status=status.HTTP_403_FORBIDDEN)
else:
return Response({'error': 'L\'utilisateur et le mot de passe de correspondent pas'}, status=status.HTTP_401_UNAUTHORIZED)
def create_or_update_device(self, user, device_id, device_model):
obj, created = Device.objects.update_or_create(
id=device_id,
device_model=device_model,
defaults={
'user': user
}
)
return obj
def create_login_log(self, user, device):
LoginLog.objects.create(user=user, device=device)
def get_username_from_email(self, email):
try:
user = CustomUser.objects.get(email=email)
return user.username
except ObjectDoesNotExist:
return None
class Logout(APIView):
permission_classes = (IsAuthenticated,)
def post(self, request, *args, **kwargs):
# request.user.auth_token.delete()
device_id = request.data.get('device_id')
if request.user.device_id == device_id:
request.user.device_id = None
request.user.save()
Device.objects.filter(id=device_id).delete()
return Response(status=status.HTTP_200_OK)
class ChangePasswordView(UpdateAPIView):
serializer_class = ChangePasswordSerializer
def update(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
# if using drf authtoken, create a new token
if hasattr(user, 'auth_token'):
user.auth_token.delete()
token, created = Token.objects.get_or_create(user=user)
# return new token
return Response({'token': token.key}, status=status.HTTP_200_OK)

@ -33,6 +33,7 @@ ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
'daphne',
'authentication',
'sync',
'tournaments',
# 'crm',

@ -1,8 +1,8 @@
from django.contrib import admin
from .models import BaseModel, ModelLog, DataAccess, Device
from django.utils import timezone
from .models import BaseModel, ModelLog, DataAccess
class SyncedObjectAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
if isinstance(obj, BaseModel):
@ -25,10 +25,6 @@ class ModelLogAdmin(admin.ModelAdmin):
ordering = ['-date']
search_fields = ['model_id']
class DeviceAdmin(admin.ModelAdmin):
list_display = ['user', 'last_login', 'id']
readonly_fields = ('last_login',)
class DataAccessAdmin(SyncedObjectAdmin):
list_display = ['related_user', 'get_shared_users', 'model_name', 'model_id']
list_filter = ['related_user', 'shared_with']
@ -41,4 +37,3 @@ class DataAccessAdmin(SyncedObjectAdmin):
# Register your models here.
admin.site.register(ModelLog, ModelLogAdmin)
admin.site.register(DataAccess, DataAccessAdmin)
admin.site.register(Device, DeviceAdmin)

@ -0,0 +1,16 @@
# Generated by Django 5.1 on 2025-03-20 14:49
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('sync', '0003_device'),
]
operations = [
migrations.DeleteModel(
name='Device',
),
]

@ -1,4 +1,3 @@
from .base import BaseModel, SideStoreModel
from .model_log import ModelLog, ModelOperation
from .data_access import DataAccess
from .device import Device

@ -2,7 +2,9 @@ from django.db.models.signals import pre_save, post_save, pre_delete, post_delet
from django.db import models, transaction
from django.dispatch import receiver
from .models import DataAccess, ModelLog, ModelOperation, BaseModel, SideStoreModel, Device
from .models import DataAccess, ModelLog, ModelOperation, BaseModel, SideStoreModel
from authentication.models import Device
from django.contrib.auth import get_user_model
from django.utils import timezone

Loading…
Cancel
Save