This commit is contained in:
Sayyid Hamid Mahdavi 2025-11-20 13:57:23 +03:30
parent 0d59a34e06
commit 2fe322241a
6 changed files with 127 additions and 27 deletions

View file

@ -0,0 +1,99 @@
# Generated by Django 5.1.4 on 2025-11-20 09:05
import django.utils.timezone
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gooyal_oauth2', '0004_application_avatar'),
]
operations = [
migrations.AlterUniqueTogether(
name='refreshtoken',
unique_together=set(),
),
migrations.AddField(
model_name='application',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='application',
name='updated_at',
field=models.DateTimeField(auto_now=True, db_index=True),
),
migrations.AddField(
model_name='grant',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='grant',
name='updated_at',
field=models.DateTimeField(auto_now=True, db_index=True),
),
migrations.AddField(
model_name='refreshtoken',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='refreshtoken',
name='updated_at',
field=models.DateTimeField(auto_now=True, db_index=True),
),
migrations.AddField(
model_name='resource',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='resource',
name='updated_at',
field=models.DateTimeField(auto_now=True, db_index=True),
),
migrations.AddField(
model_name='scope',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='scope',
name='updated_at',
field=models.DateTimeField(auto_now=True, db_index=True),
),
migrations.AlterField(
model_name='application',
name='uuid',
field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
),
migrations.AlterField(
model_name='grant',
name='uuid',
field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
),
migrations.AlterField(
model_name='refreshtoken',
name='uuid',
field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
),
migrations.AlterField(
model_name='resource',
name='uuid',
field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
),
migrations.AlterField(
model_name='scope',
name='uuid',
field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
),
]

View file

@ -16,14 +16,14 @@ from django.conf import settings
from django.db import models, router, transaction from django.db import models, router, transaction
from django.utils import timezone from django.utils import timezone
from utils.models import BaseModel
from .settings import oauth2_settings from .settings import oauth2_settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Resource(models.Model): class Resource(BaseModel):
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
user = models.ForeignKey( user = models.ForeignKey(
@ -36,13 +36,12 @@ class Resource(models.Model):
return self.name return self.name
class Application(AbstractApplication): class Application(AbstractApplication, BaseModel):
""" """
Application model for use with Django OAuth Toolkit that allows the scopes Application model for use with Django OAuth Toolkit that allows the scopes
available to an application to be restricted on a per-application basis. available to an application to be restricted on a per-application basis.
""" """
id=None id = None
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
name = models.CharField(max_length=255, blank=False, unique=True) name = models.CharField(max_length=255, blank=False, unique=True)
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
@ -74,14 +73,12 @@ class Application(AbstractApplication):
allowed_scopes = self.allowed_scopes allowed_scopes = self.allowed_scopes
return Scope.objects.filter(name__in=allowed_scopes).order_by('name') return Scope.objects.filter(name__in=allowed_scopes).order_by('name')
def allows_grant_type(self, *grant_types): def allows_grant_type(self, *grant_types):
# Assume, for this example, that self.authorization_grant_type is set to self.GRANT_AUTHORIZATION_CODE # Assume, for this example, that self.authorization_grant_type is set to self.GRANT_AUTHORIZATION_CODE
return bool(set([self.authorization_grant_type, self.GRANT_CLIENT_CREDENTIALS]) & set(grant_types)) return bool(set([self.authorization_grant_type, self.GRANT_CLIENT_CREDENTIALS]) & set(grant_types))
class Scope(models.Model): class Scope(BaseModel):
""" """
Django model for an OAuth scope. Django model for an OAuth scope.
""" """
@ -89,8 +86,6 @@ class Scope(models.Model):
# NOTE: This is not used to limit access to the scope in any way - we want the # NOTE: This is not used to limit access to the scope in any way - we want the
# scope to be available to other applications in order to request access # scope to be available to other applications in order to request access
# to the resource it protects! # to the resource it protects!
id = None
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
application = models.ForeignKey( application = models.ForeignKey(
oauth2_settings.APPLICATION_MODEL, oauth2_settings.APPLICATION_MODEL,
models.CASCADE, models.CASCADE,
@ -177,17 +172,15 @@ class Scope(models.Model):
return True return True
class Grant(AbstractGrant): class Grant(AbstractGrant, BaseModel):
id = None id = None
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
application = models.ForeignKey( application = models.ForeignKey(
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, related_name='grants' oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, related_name='grants'
) )
class RefreshToken(AbstractRefreshToken): class RefreshToken(BaseModel, AbstractRefreshToken):
id = None id = None
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
application = models.ForeignKey( application = models.ForeignKey(
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, related_name='refresh_tokens') oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, related_name='refresh_tokens')
@ -215,7 +208,6 @@ class RefreshToken(AbstractRefreshToken):
self.save() self.save()
class AccessToken(AbstractAccessToken): class AccessToken(AbstractAccessToken):
id = None id = None
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True) uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
@ -240,7 +232,7 @@ def clear_expired():
flat_queryset = queryset.values_list("pk", flat=True)[:CLEAR_EXPIRED_TOKENS_BATCH_SIZE] flat_queryset = queryset.values_list("pk", flat=True)[:CLEAR_EXPIRED_TOKENS_BATCH_SIZE]
batch_length = flat_queryset.count() batch_length = flat_queryset.count()
queryset.model.objects.filter(pk__in=list(flat_queryset)).delete() queryset.model.objects.filter(pk__in=list(flat_queryset)).delete()
logger.debug(f"{batch_length} tokens deleted, {current_no-batch_length} left") logger.debug(f"{batch_length} tokens deleted, {current_no - batch_length} left")
queryset = queryset.model.objects.filter(query) queryset = queryset.model.objects.filter(query)
time.sleep(CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL) time.sleep(CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL)
current_no = queryset.count() current_no = queryset.count()
@ -298,4 +290,3 @@ def clear_expired():
grants_deleted_no = batch_delete(grants, grants_query) grants_deleted_no = batch_delete(grants, grants_query)
logger.info("%s Expired grant tokens deleted", grants_deleted_no) logger.info("%s Expired grant tokens deleted", grants_deleted_no)

View file

@ -31,3 +31,9 @@ class ApplicationViewSet(ReadOnlyModelViewSet):
else: else:
raise Http404() raise Http404()
# @action(detail=True, permission_classes=[AllowAny])
# def tokens(self, request, pk=None):
# app: Application = self.get_object()

View file

@ -2,7 +2,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
MAX_OTP_TRY = 3 MAX_OTP_TRY = 3
DEVELOPMENT_PHONE_NUMBERS = ['+989999999999', '+989999999998'] DEVELOPMENT_PHONE_NUMBERS = []
MAX_OTP_VALID_DURATION = 120 MAX_OTP_VALID_DURATION = 120
class GenderChoices(models.TextChoices): class GenderChoices(models.TextChoices):

View file

@ -166,6 +166,7 @@ class User(AbstractUser):
self.otp_expire = timezone.now() + timedelta(seconds=MAX_OTP_VALID_DURATION) self.otp_expire = timezone.now() + timedelta(seconds=MAX_OTP_VALID_DURATION)
self.otp_try = 0 self.otp_try = 0
self.save()
def set_ott(self): def set_ott(self):
self.ott = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32)) self.ott = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32))
@ -180,18 +181,22 @@ class User(AbstractUser):
def check_otp(self, otp): def check_otp(self, otp):
if self.otp_try <= MAX_OTP_TRY: if self.otp_try <= MAX_OTP_TRY:
result = otp and check_password(self.otp, otp) and self.otp_is_valid() try:
result = otp and check_password(otp, self.otp) and self.otp_is_valid()
except Exception as e:
result = False
if result: if result:
self.otp = None self.otp = None
self.date_joined = timezone.now() if not self.date_joined:
self.date_joined = timezone.now()
else: else:
self.otp_try += 1 self.otp_try += 1
self.save()
else: else:
self.otp = None
result = False result = False
self.save()
return result return result
def check_ott(self, ott: str): def check_ott(self, ott: str):

View file

@ -73,13 +73,12 @@ class RequestOTPSerializer(serializers.ModelSerializer):
try: try:
user.set_otp() user.set_otp()
except Exception as e: except Exception as e:
raise UnprocessableEntity() raise UnprocessableEntity('otp expire time not reached')
user.save()
user.send_otp() user.send_otp()
else: else:
raise UnprocessableEntity() raise UnprocessableEntity('otp expire time not reached yet')
self.instance = user self.instance = user
return user return user