diff --git a/api/desecapi/migrations/0038_domainserial_and_more.py b/api/desecapi/migrations/0038_domainserial_and_more.py new file mode 100644 index 000000000..fde72df52 --- /dev/null +++ b/api/desecapi/migrations/0038_domainserial_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 5.0 on 2023-12-15 16:09 + +import django.db.models.deletion +import django_prometheus.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("desecapi", "0037_remove_tokendomainpolicy_perm_dyndns"), + ] + + operations = [ + migrations.CreateModel( + name="DomainSerial", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True, db_index=True)), + ("updated", models.DateTimeField(null=True)), + ("node", models.CharField(max_length=255)), + ("serial", models.PositiveBigIntegerField(null=True)), + ( + "domain", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="desecapi.domain", + ), + ), + ], + bases=( + django_prometheus.models.ExportModelOperationsMixin("DomainSerial"), + models.Model, + ), + ), + migrations.AddConstraint( + model_name="domainserial", + constraint=models.UniqueConstraint( + fields=("domain", "node", "updated", "serial"), + name="domain_serial_unique_policy", + nulls_distinct=False, + ), + ), + ] diff --git a/api/desecapi/models/__init__.py b/api/desecapi/models/__init__.py index 99a158a0a..930f605c3 100644 --- a/api/desecapi/models/__init__.py +++ b/api/desecapi/models/__init__.py @@ -2,7 +2,7 @@ from .authenticated_actions import * from .base import validate_domain_name, validate_lower, validate_upper from .captcha import Captcha -from .domains import Domain +from .domains import Domain, DomainSerial from .donation import Donation from .mfa import BaseFactor, TOTPFactor from .records import ( diff --git a/api/desecapi/models/domains.py b/api/desecapi/models/domains.py index d849d7c3a..47a499c2b 100644 --- a/api/desecapi/models/domains.py +++ b/api/desecapi/models/domains.py @@ -297,3 +297,29 @@ def delete(self, *args, **kwargs): def __str__(self): return self.name + + +class DomainSerial(ExportModelOperationsMixin("DomainSerial"), models.Model): + created = models.DateTimeField(auto_now_add=True, db_index=True) + updated = models.DateTimeField(null=True) + domain = models.ForeignKey("Domain", on_delete=models.CASCADE) + node = models.CharField(max_length=255) + serial = models.PositiveBigIntegerField(null=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + name="domain_serial_unique_policy", + fields=["domain", "node", "updated", "serial"], + nulls_distinct=False, + ), + ] + + def __str__(self): + return "" % ( + self.pk, + self.domain.name, + self.node, + self.serial, + self.updated, + ) diff --git a/api/desecapi/serializers/domains.py b/api/desecapi/serializers/domains.py index b9448e1ab..67069360b 100644 --- a/api/desecapi/serializers/domains.py +++ b/api/desecapi/serializers/domains.py @@ -3,7 +3,7 @@ from rest_framework import serializers from api import settings -from desecapi.models import Domain, RR_SET_TYPES_AUTOMATIC +from desecapi.models import Domain, DomainSerial, RR_SET_TYPES_AUTOMATIC from desecapi.validators import ReadOnlyOnUpdateValidator from .records import RRsetSerializer @@ -170,3 +170,24 @@ def fqdn(idx): rrset_list_serializer.save() return domain + + +class DomainSerialSerializer(serializers.ModelSerializer): + class Meta: + model = DomainSerializer + fields = ( + "created", + "updated", + "domain", + "node", + "serial", + ) + read_only_fields = fields + + def create(self, validated_data): + # TODO + self.domain.domainserial_set.bulk_create( + [DomainSerial(domain=self.domain, node=node) for node in NODES], + ignore_conflicts=True, + ) + return None # TODO diff --git a/api/desecapi/views/domains.py b/api/desecapi/views/domains.py index 529ecbb88..8f31f8552 100644 --- a/api/desecapi/views/domains.py +++ b/api/desecapi/views/domains.py @@ -110,6 +110,36 @@ def zonefile(self, request, name=None): return Response(prefix + instance.zonefile, content_type="text/dns") +class DomainSerialViewSet( + mixins.CreateModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): + serializer_class = DomainSerialSerializer + lookup_field = "name" + lookup_value_regex = r"[^/]+" + permission_classes = [ + IsAuthenticated, + permissions.MFARequiredIfEnabled, + permissions.IsOwner, + ] + + @property + def throttle_scope(self): + return ( + "dns_api_cheap" + if self.request.method in SAFE_METHODS + else "dns_api_per_domain_expensive" + ) + + @property + def domain(self): + return self.get_object() + + def get_queryset(self): + return self.domain.domainserial_set.all() # TODO + + class SerialListView(APIView): permission_classes = (permissions.IsVPNClient,) throttle_classes = (