mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
Merge pull request #2478 from modoboa/feature/transport_crud
WIP: relaydomain CRUD in new admin
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import repository from './repository'
|
||||
|
||||
const resource = 'transports'
|
||||
|
||||
export default {
|
||||
getAll () {
|
||||
return repository.get(`/${resource}/`)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@
|
||||
<template v-slot:form.options="{ step }">
|
||||
<domain-options-form :ref="`form_${step}`" :domain="domain" @createAdmin="updateCreateAdmin" />
|
||||
</template>
|
||||
<template v-slot:form.transport="{ step }">
|
||||
<domain-transport-form :ref="`form_${step}`" :domain="domain" />
|
||||
</template>
|
||||
<template v-slot:item.with_random_password="{ item }">
|
||||
<template v-if="item.value">
|
||||
<v-col cols="12" class="highligth white--text">
|
||||
@@ -52,6 +55,7 @@ import DomainDNSForm from './DomainDNSForm'
|
||||
import DomainGeneralForm from './DomainGeneralForm'
|
||||
import DomainLimitationsForm from './DomainLimitationsForm'
|
||||
import DomainOptionsForm from './DomainOptionsForm'
|
||||
import DomainTransportForm from './DomainTransportForm'
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
@@ -60,7 +64,8 @@ export default {
|
||||
'domain-dns-form': DomainDNSForm,
|
||||
DomainGeneralForm,
|
||||
DomainLimitationsForm,
|
||||
DomainOptionsForm
|
||||
DomainOptionsForm,
|
||||
DomainTransportForm
|
||||
},
|
||||
computed: {
|
||||
summarySections () {
|
||||
@@ -87,12 +92,6 @@ export default {
|
||||
{ key: this.$gettext('Default mailbox quota'), value: this.domain.default_mailbox_quota },
|
||||
{ key: this.$gettext('Message sending limit'), value: this.domain.message_sending_limit }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('Options'),
|
||||
items: [
|
||||
{ key: this.$gettext('Create a domain administrator'), value: this.createAdmin, type: 'yesno' }
|
||||
]
|
||||
}
|
||||
]
|
||||
if (this.domain.enable_dkim) {
|
||||
@@ -100,25 +99,53 @@ export default {
|
||||
result[1].items.push({ key: this.$gettext('DKIM key length'), value: this.domain.dkim_key_length })
|
||||
}
|
||||
if (this.createAdmin) {
|
||||
result[3].items.push({ key: this.$gettext('Administrator name'), value: this.domain.domain_admin.username })
|
||||
result[3].items.push({
|
||||
name: 'with_random_password',
|
||||
key: this.$gettext('Random password'),
|
||||
value: this.domain.domain_admin.with_random_password,
|
||||
type: 'yesno'
|
||||
})
|
||||
result[3].items.push({
|
||||
key: this.$gettext('With mailbox'),
|
||||
value: this.domain.domain_admin.with_mailbox,
|
||||
type: 'yesno'
|
||||
})
|
||||
result[3].items.push({
|
||||
key: this.$gettext('Create aliases'),
|
||||
value: this.domain.domain_admin.with_aliases,
|
||||
type: 'yesno'
|
||||
result.push({
|
||||
title: this.$gettext('Options'),
|
||||
items: [
|
||||
{ key: this.$gettext('Create a domain administrator'), value: this.createAdmin, type: 'yesno' },
|
||||
{ key: this.$gettext('Administrator name'), value: this.domain.domain_admin.username },
|
||||
{
|
||||
name: 'with_random_password',
|
||||
key: this.$gettext('Random password'),
|
||||
value: this.domain.domain_admin.with_random_password,
|
||||
type: 'yesno'
|
||||
},
|
||||
{
|
||||
key: this.$gettext('With mailbox'),
|
||||
value: this.domain.domain_admin.with_mailbox,
|
||||
type: 'yesno'
|
||||
},
|
||||
{
|
||||
key: this.$gettext('Create aliases'),
|
||||
value: this.domain.domain_admin.with_aliases,
|
||||
type: 'yesno'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
if (this.domain.type === 'relaydomain') {
|
||||
const relayEntry = {
|
||||
title: this.$gettext('Transport'),
|
||||
items: [
|
||||
{ key: this.$gettext('Service'), value: this.domain.transport.service }
|
||||
]
|
||||
}
|
||||
if (this.$refs.form_4.service) {
|
||||
const service = this.$refs.form_4.service
|
||||
for (const setting of service.settings) {
|
||||
const item = { key: setting.label, value: this.domain.transport.settings[`${service.name}_${setting.name}`] }
|
||||
if (setting.type === 'boolean') {
|
||||
item.type = 'yesno'
|
||||
}
|
||||
relayEntry.items.push(item)
|
||||
}
|
||||
}
|
||||
result.push(relayEntry)
|
||||
}
|
||||
return result
|
||||
},
|
||||
steps () {
|
||||
return this.domain.type === 'domain' ? this.domainSteps : this.relaySteps
|
||||
}
|
||||
},
|
||||
data () {
|
||||
@@ -128,11 +155,17 @@ export default {
|
||||
domain_admin: {}
|
||||
},
|
||||
formErrors: {},
|
||||
steps: [
|
||||
domainSteps: [
|
||||
{ name: 'general', title: this.$gettext('General') },
|
||||
{ name: 'dns', title: this.$gettext('DNS') },
|
||||
{ name: 'limitations', title: this.$gettext('Limitations') },
|
||||
{ name: 'options', title: this.$gettext('Options') }
|
||||
],
|
||||
relaySteps: [
|
||||
{ name: 'general', title: this.$gettext('General') },
|
||||
{ name: 'dns', title: this.$gettext('DNS') },
|
||||
{ name: 'limitations', title: this.$gettext('Limitations') },
|
||||
{ name: 'transport', title: this.$gettext('Transport') }
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -160,7 +193,8 @@ export default {
|
||||
with_random_password: false,
|
||||
with_mailbox: false,
|
||||
with_aliases: false
|
||||
}
|
||||
},
|
||||
transport: {}
|
||||
}
|
||||
},
|
||||
close () {
|
||||
@@ -181,6 +215,9 @@ export default {
|
||||
if (!this.createAdmin) {
|
||||
delete data.domain_admin
|
||||
}
|
||||
if (data.type === 'relaydomain') {
|
||||
this.$refs.form_4.checkSettingTypes(data)
|
||||
}
|
||||
this.$store.dispatch('domains/createDomain', data).then(resp => {
|
||||
this.$router.push({ name: 'DomainDetail', params: { id: resp.data.pk } })
|
||||
bus.$emit('notification', { msg: this.$gettext('Domain created') })
|
||||
|
||||
@@ -104,6 +104,35 @@
|
||||
<domain-limitations-form :domain="editedDomain" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel v-if="editedDomain.type === 'relaydomain'">
|
||||
<v-expansion-panel-header v-slot="{ open }">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="4">
|
||||
<translate>Transport</translate>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<v-fade-transition leave-absolute>
|
||||
<span v-if="open"></span>
|
||||
<v-row
|
||||
v-else
|
||||
no-gutters
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-col cols="6">
|
||||
<translate class="mr-2">Service: </translate> {{ editedDomain.transport.service }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<domain-transport-form ref="transportForm" :domain="editedDomain" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<div class="mt-4 d-flex justify-end">
|
||||
<v-btn color="grey lighten-1" @click="$router.go(-1)">
|
||||
@@ -121,12 +150,14 @@ import { bus } from '@/main'
|
||||
import DomainDNSForm from './DomainDNSForm'
|
||||
import DomainGeneralForm from './DomainGeneralForm'
|
||||
import DomainLimitationsForm from './DomainLimitationsForm'
|
||||
import DomainTransportForm from './DomainTransportForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'domain-dns-form': DomainDNSForm,
|
||||
DomainGeneralForm,
|
||||
DomainLimitationsForm
|
||||
DomainLimitationsForm,
|
||||
DomainTransportForm
|
||||
},
|
||||
props: ['domain'],
|
||||
data () {
|
||||
@@ -143,6 +174,9 @@ export default {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (this.editedDomain.type === 'relaydomain') {
|
||||
this.$refs.transportForm.checkSettingTypes(this.editedDomain)
|
||||
}
|
||||
this.$store.dispatch('domains/updateDomain', this.editedDomain).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Domain updated') })
|
||||
})
|
||||
@@ -156,6 +190,11 @@ export default {
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
'editedDomain.type' (val) {
|
||||
if (val === 'relaydomain' && this.editedDomain.transport === null) {
|
||||
this.$set(this.editedDomain, 'transport', {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,14 @@
|
||||
<router-link :to="{ name: 'DomainDetail', params: { id: item.pk } }">
|
||||
{{ item.name }}
|
||||
</router-link>
|
||||
<v-chip
|
||||
v-if="item.type === 'relaydomain'"
|
||||
small
|
||||
color="primary"
|
||||
class="ml-2"
|
||||
>
|
||||
<translate>Relay</translate>
|
||||
</v-chip>
|
||||
<translate v-if="!item.enabled" class="ml-2 grey--text">(disabled)</translate>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div>
|
||||
<validation-observer ref="observer">
|
||||
<label class="m-label"><translate>Service</translate></label>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<v-autocomplete
|
||||
v-model="service"
|
||||
:items="backends"
|
||||
item-text="name"
|
||||
return-object
|
||||
outlined
|
||||
@change="resetForm"
|
||||
:error-messages="errors"
|
||||
/>
|
||||
</validation-provider>
|
||||
<template v-if="service">
|
||||
<template v-for="setting in service.settings">
|
||||
<div :key="setting.name">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
:rules="setting.required ? 'required' : ''"
|
||||
>
|
||||
<template
|
||||
v-if="setting.type === 'str' || setting.type === 'int'"
|
||||
>
|
||||
<label class="m-label">{{ setting.label }}</label>
|
||||
<v-text-field
|
||||
v-model="domain.transport.settings[`${service.name}_${setting.name}`]"
|
||||
outlined
|
||||
:error-messages="errors"
|
||||
:type="setting.type === 'int' ? 'number' : 'text'"
|
||||
/>
|
||||
</template>
|
||||
<v-checkbox
|
||||
v-else-if="setting.type === 'boolean'"
|
||||
v-model="domain.transport.settings[`${service.name}_${setting.name}`]"
|
||||
:label="setting.label"
|
||||
:error-messages="errors"
|
||||
/>
|
||||
</validation-provider>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</validation-observer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import transports from '@/api/transports'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
domain: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
backends: [],
|
||||
service: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetForm (value) {
|
||||
this.$set(this.domain, 'transport', { service: value.name, settings: {} })
|
||||
for (const setting of value.settings) {
|
||||
if (setting.default) {
|
||||
let defaultValue = setting.default
|
||||
if (setting.type === 'int') {
|
||||
defaultValue = parseInt(defaultValue)
|
||||
} else if (setting.type === 'boolean') {
|
||||
if (defaultValue === 'False') {
|
||||
defaultValue = false
|
||||
} else {
|
||||
defaultValue = true
|
||||
}
|
||||
}
|
||||
this.$set(this.domain.transport.settings, `${value.name}_${setting.name}`, defaultValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
checkSettingTypes (data) {
|
||||
for (const setting of this.service.settings) {
|
||||
if (setting.type === 'int') {
|
||||
const fullName = `${this.service.name}_${setting.name}`
|
||||
data.transport.settings[fullName] = parseInt(data.transport.settings[fullName])
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
transports.getAll().then(resp => {
|
||||
this.backends = resp.data
|
||||
if (this.domain) {
|
||||
this.service = this.backends.find(backend => backend.name === this.domain.transport.service)
|
||||
}
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -18,6 +18,8 @@ from modoboa.lib import fields as lib_fields
|
||||
from modoboa.lib import validators, web_utils
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
from modoboa.parameters import tools as param_tools
|
||||
from modoboa.transport import models as transport_models
|
||||
from modoboa.transport.api.v2 import serializers as transport_serializers
|
||||
|
||||
from ... import constants, models
|
||||
|
||||
@@ -34,17 +36,33 @@ class CreateDomainAdminSerializer(serializers.Serializer):
|
||||
class DomainSerializer(v1_serializers.DomainSerializer):
|
||||
"""Domain serializer for v2 API."""
|
||||
|
||||
domain_admin = CreateDomainAdminSerializer(required=False)
|
||||
domain_admin = CreateDomainAdminSerializer(required=False, write_only=True)
|
||||
transport = transport_serializers.TransportSerializer(required=False)
|
||||
|
||||
class Meta(v1_serializers.DomainSerializer.Meta):
|
||||
fields = v1_serializers.DomainSerializer.Meta.fields + (
|
||||
"domain_admin",
|
||||
"domain_admin", "transport"
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
result = super().validate(data)
|
||||
dtype = data.get("type", "domain")
|
||||
if dtype == "relaydomain" and not data.get("transport"):
|
||||
raise serializers.ValidationError({"transport": _("This field is required")})
|
||||
return result
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create administrator and other stuff if needed."""
|
||||
domain_admin = validated_data.pop("domain_admin", None)
|
||||
transport_def = validated_data.pop("transport", None)
|
||||
domain = super().create(validated_data)
|
||||
if transport_def:
|
||||
transport = transport_models.Transport(**transport_def)
|
||||
transport.pattern = domain.name
|
||||
transport.save()
|
||||
domain.transport = transport
|
||||
domain.save()
|
||||
|
||||
if not domain_admin:
|
||||
return domain
|
||||
|
||||
@@ -99,6 +117,27 @@ class DomainSerializer(v1_serializers.DomainSerializer):
|
||||
domain.add_admin(da)
|
||||
return domain
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Update domain and create/update transport."""
|
||||
transport_def = validated_data.pop("transport", None)
|
||||
super().update(instance, validated_data)
|
||||
if transport_def and instance.type == "relaydomain":
|
||||
transport = getattr(instance, "transport", None)
|
||||
created = False
|
||||
if transport:
|
||||
for key, value in transport_def.items():
|
||||
setattr(instance, key, value)
|
||||
transport._settings = transport_def["_settings"]
|
||||
else:
|
||||
transport = transport_models.Transport(**transport_def)
|
||||
created = True
|
||||
transport.pattern = instance.name
|
||||
transport.save()
|
||||
if created:
|
||||
instance.transport = transport
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class DeleteDomainSerializer(serializers.Serializer):
|
||||
"""Serializer used with delete operation."""
|
||||
|
||||
@@ -41,6 +41,33 @@ class DomainViewSetTestCase(ModoAPITestCase):
|
||||
address="postmaster@domain.tld").exists()
|
||||
)
|
||||
|
||||
def test_update(self):
|
||||
domain = models.Domain.objects.get(name="test2.com")
|
||||
data = {
|
||||
"name": "test2.com",
|
||||
"type": "relaydomain",
|
||||
"transport": {
|
||||
"service": "relay",
|
||||
"settings": {
|
||||
"relay_target_port": 25,
|
||||
"relay_target_host": "localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
url = reverse("v2:domain-detail", args=[domain.pk])
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
domain.refresh_from_db()
|
||||
self.assertEqual(domain.transport.service, data["transport"]["service"])
|
||||
self.assertEqual(
|
||||
domain.transport._settings["relay_target_host"],
|
||||
data["transport"]["settings"]["relay_target_host"]
|
||||
)
|
||||
|
||||
data["transport"]["relay_verify_recipients"] = True
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_delete(self):
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION="Token " + self.da_token.key)
|
||||
|
||||
@@ -58,15 +58,13 @@ class DomainViewSet(lib_viewsets.RevisionModelMixin,
|
||||
return models.Domain.objects.get_for_admin(self.request.user)
|
||||
|
||||
def get_serializer_class(self, *args, **kwargs):
|
||||
if self.action == "create":
|
||||
return serializers.DomainSerializer
|
||||
if self.action == "delete":
|
||||
return serializers.DeleteDomainSerializer
|
||||
if self.action == "administrators":
|
||||
return serializers.DomainAdminSerializer
|
||||
if self.action in ["add_administrator", "remove_administrator"]:
|
||||
return serializers.SimpleDomainAdminSerializer
|
||||
return v1_serializers.DomainSerializer
|
||||
return serializers.DomainSerializer
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def delete(self, request, **kwargs):
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Transport related serializers."""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ... import backends
|
||||
from ... import models
|
||||
|
||||
|
||||
class TransportBackendSettingSerializer(serializers.Serializer):
|
||||
"""Serializer for transport backend setting."""
|
||||
|
||||
name = serializers.CharField()
|
||||
label = serializers.CharField()
|
||||
type = serializers.CharField(default="str")
|
||||
required = serializers.BooleanField(default=True)
|
||||
default = serializers.CharField(default=None)
|
||||
|
||||
|
||||
class TransportBackendSerializer(serializers.Serializer):
|
||||
"""Serializer for transport backend."""
|
||||
|
||||
name = serializers.CharField()
|
||||
settings = TransportBackendSettingSerializer(many=True)
|
||||
|
||||
|
||||
class TransportSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Transport model."""
|
||||
|
||||
settings = serializers.DictField(source="_settings")
|
||||
|
||||
class Meta:
|
||||
fields = ("service", "settings")
|
||||
model = models.Transport
|
||||
|
||||
def validate(self, data):
|
||||
self.backend = backends.manager.get_backend(data["service"])
|
||||
errors = {}
|
||||
for field, error in self.backend.clean_fields(data["_settings"]):
|
||||
errors[field] = error
|
||||
if errors:
|
||||
raise serializers.ValidationError(errors)
|
||||
return data
|
||||
@@ -0,0 +1,16 @@
|
||||
"""API v2 tests."""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from modoboa.lib.tests import ModoAPITestCase
|
||||
|
||||
|
||||
class TransportViewSetTestCase(ModoAPITestCase):
|
||||
|
||||
def test_list(self):
|
||||
url = reverse("v2:transport-list")
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
backends = resp.json()
|
||||
self.assertEqual(len(backends), 1)
|
||||
self.assertEqual(backends[0]["name"], "relay")
|
||||
@@ -0,0 +1,12 @@
|
||||
"""App related urls."""
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import viewsets
|
||||
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(
|
||||
r"transports", viewsets.TransportViewSet, basename="transport")
|
||||
|
||||
urlpatterns = router.urls
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Transport related viewsets."""
|
||||
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import permissions, response, viewsets
|
||||
|
||||
from . import serializers
|
||||
from ... import backends
|
||||
|
||||
|
||||
class TransportViewSet(viewsets.ViewSet):
|
||||
"""Viewset for Transport."""
|
||||
|
||||
permissions = (permissions.IsAuthenticated, )
|
||||
|
||||
@extend_schema(
|
||||
responses={200: serializers.TransportBackendSerializer}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Return the list of all defined transport backends."""
|
||||
all_settings = backends.manager.get_all_backend_settings()
|
||||
result = []
|
||||
for name, settings in all_settings.items():
|
||||
serializer = serializers.TransportBackendSerializer(
|
||||
{"name": name, "settings": settings}
|
||||
)
|
||||
result.append(serializer.data)
|
||||
return response.Response(result)
|
||||
@@ -14,6 +14,7 @@ urlpatterns = [
|
||||
path('', include("modoboa.relaydomains.api.v1.urls")),
|
||||
path('', include("modoboa.dnstools.api.v2.urls")),
|
||||
path('', include("modoboa.maillog.api.v2.urls")),
|
||||
path('', include("modoboa.transport.api.v2.urls")),
|
||||
]
|
||||
|
||||
urlpatterns += exts_pool.get_urls(category="api")
|
||||
|
||||
Reference in New Issue
Block a user