diff --git a/api/funkwhale_api/common/validators.py b/api/funkwhale_api/common/validators.py index b5f26cac5421450fccaac039720e1238cf118ca8..78a4b4c7c4c3c9bf40d33616f4e0538ba7ca6ebc 100644 --- a/api/funkwhale_api/common/validators.py +++ b/api/funkwhale_api/common/validators.py @@ -1,6 +1,7 @@ import mimetypes from os.path import splitext +from django.core import validators from django.core.exceptions import ValidationError from django.core.files.images import get_image_dimensions from django.template.defaultfilters import filesizeformat @@ -150,3 +151,17 @@ class FileValidator(object): } raise ValidationError(message) + + +class DomainValidator(validators.URLValidator): + message = "Enter a valid domain name." + + def __call__(self, value): + """ + This is a bit hackish but since we don't have any built-in domain validator, + we use the url one, and prepend http:// in front of it. + + If it fails, we know the domain is not valid. + """ + super().__call__("http://{}".format(value)) + return value diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 2fdeaaa7675abf7ccef9e3a2ce05319682ea0f85..59360aea10374ff2d4e804008fb9f62aeecb156f 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -13,6 +13,7 @@ from django.urls import reverse from funkwhale_api.common import session from funkwhale_api.common import utils as common_utils +from funkwhale_api.common import validators as common_validators from funkwhale_api.music import utils as music_utils from . import utils as federation_utils @@ -83,7 +84,11 @@ class DomainQuerySet(models.QuerySet): class Domain(models.Model): - name = models.CharField(primary_key=True, max_length=255) + name = models.CharField( + primary_key=True, + max_length=255, + validators=[common_validators.DomainValidator()], + ) creation_date = models.DateTimeField(default=timezone.now) nodeinfo_fetch_date = models.DateTimeField(default=None, null=True, blank=True) nodeinfo = JSONField(default=empty_dict, max_length=50000, blank=True) diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index 0697c6c14e3609fc32c508807312d47913e11407..763b37497f44b405d85f1644fd68ffaddb4fa3d1 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -98,7 +98,10 @@ class ManageInvitationViewSet( class ManageDomainViewSet( - mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet + mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, ): lookup_value_regex = r"[a-zA-Z0-9\-\.]+" queryset = ( diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 803820b489350fcbf795abb424bace0f3e3b614c..74ba96ba8a595a94fb7ce94968791fec0fda4567 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -1,3 +1,5 @@ +import pytest + from funkwhale_api.manage import serializers @@ -53,6 +55,13 @@ def test_manage_domain_serializer(factories, now): assert s.data == expected +def test_manage_domain_serializer_validates_hostname(db): + s = serializers.ManageDomainSerializer(data={"name": "hello world"}) + + with pytest.raises(serializers.serializers.ValidationError): + s.is_valid(raise_exception=True) + + def test_manage_actor_serializer(factories, now): actor = factories["federation.Actor"]() setattr(actor, "uploads_count", 66) diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py index 72e945bcad5186c2f6919b3db8bffa15aad54ef5..4591f7b1bca88f5ec2b6c7b28a78a7c3329464fa 100644 --- a/api/tests/manage/test_views.py +++ b/api/tests/manage/test_views.py @@ -1,6 +1,7 @@ import pytest from django.urls import reverse +from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import tasks as federation_tasks from funkwhale_api.manage import serializers, views @@ -90,6 +91,14 @@ def test_domain_detail(factories, superuser_api_client): assert response.data["name"] == d.pk +def test_domain_create(superuser_api_client): + url = reverse("api:v1:manage:federation:domains-list") + response = superuser_api_client.post(url, {"name": "test.federation"}) + + assert response.status_code == 201 + assert federation_models.Domain.objects.filter(pk="test.federation").exists() + + def test_domain_nodeinfo(factories, superuser_api_client, mocker): domain = factories["federation.Domain"]() url = reverse( diff --git a/front/src/views/admin/moderation/DomainsList.vue b/front/src/views/admin/moderation/DomainsList.vue index 84fb1df4300dffecc15db22048bafc50fb0e27dc..259d05f0cecbdb1019d59201d38d374cbe155447 100644 --- a/front/src/views/admin/moderation/DomainsList.vue +++ b/front/src/views/admin/moderation/DomainsList.vue @@ -1,26 +1,70 @@ <template> <main v-title="labels.domains"> <section class="ui vertical stripe segment"> - <h2 class="ui header"><translate>Domains</translate></h2> - <div class="ui hidden divider"></div> + <h2 class="ui left floated header"><translate>Domains</translate></h2> + <form class="ui right floated form" @submit.prevent="createDomain"> + <div v-if="errors && errors.length > 0" class="ui negative message"> + <div class="header"><translate>Error while creating domain</translate></div> + <ul class="list"> + <li v-for="error in errors">{{ error }}</li> + </ul> + </div> + <div class="inline fields"> + <div class="field"> + <label for="domain"><translate>Add a domain</translate></label> + <input type="text" id="domain" v-model="domainName"> + </div> + <div class="field"> + <button :class="['ui', {'loading': isCreating}, 'green', 'button']" type="submit" :disabled="isCreating"> + <label for="domain"><translate>Add</translate></label> + </button> + </div> + </div> + </form> + <div class="ui clearing hidden divider"></div> <domains-table></domains-table> </section> </main> </template> <script> -import DomainsTable from "@/components/manage/moderation/DomainsTable" +import axios from 'axios' +import DomainsTable from "@/components/manage/moderation/DomainsTable" export default { components: { DomainsTable }, + data () { + return { + domainName: '', + isCreating: false, + errors: [] + } + }, computed: { labels() { return { domains: this.$gettext("Domains") } } + }, + methods: { + createDomain () { + let self = this + this.isCreating = true + this.errors = [] + axios.post('manage/federation/domains/', {name: this.domainName}).then((response) => { + this.isCreating = false + this.$router.push({ + name: "manage.moderation.domains.detail", + params: {'id': response.data.name} + }) + }, (error) => { + self.isCreating = false + self.errors = error.backendErrors + }) + } } } </script>