import uuid import random from datetime import timedelta from django.utils import timezone from django.db import models, transaction from django.urls import reverse from django.conf import settings def generate_identifier(): return uuid.uuid4().hex def random_char_challenge(): chars, ret = "abcdefghijklmnopqrstuvwxyz", "" for i in range(4): ret += random.choice(chars) return ret.upper(), ret class CaptchaStore(models.Model): identifier = models.CharField(max_length=64, unique=True, default=generate_identifier) challenge = models.CharField(max_length=32) response = models.CharField(max_length=32) attempts = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() used_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = 'pingcap_captcha_store' @classmethod def generate_challenge(cls) -> 'CaptchaStore': challenge, ret = random_char_challenge() store = cls.objects.create( challenge=challenge, response=ret, expires_at=timezone.now() + timedelta(minutes=10) ) return store MAX_ATTEMPTS = 10 @classmethod def verify(cls, key, response): try: with transaction.atomic(): store = cls.objects.select_for_update().get( identifier=key, expires_at__gt=timezone.now(), used_at__isnull=True, ) if store.attempts >= cls.MAX_ATTEMPTS: return False passed = response.lower() == store.response.lower() if passed: store.used_at = timezone.now() store.attempts += 1 store.save() return passed except cls.DoesNotExist: return False def to_frontend(self): return { "key": self.identifier, "image_url": f"{settings.SSO_PROVIDER_ENDPOINT}{reverse('captcha-image', args=[self.identifier])}", }