mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Added DNS Detail page.
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"js-cookie": "^2.2.1",
|
||||
"luxon": "^1.26.0",
|
||||
"vee-validate": "^3.4.5",
|
||||
"vue": "^2.6.11",
|
||||
"vue-cookie": "^1.1.4",
|
||||
|
||||
@@ -7,6 +7,9 @@ export default {
|
||||
getDomains () {
|
||||
return repository.get(`/${domainResource}/`)
|
||||
},
|
||||
getDomainDNSDetail (domainId) {
|
||||
return repository.get(`/${domainResource}/${domainId}/dns_detail/`)
|
||||
},
|
||||
getDomainAliases (domain) {
|
||||
let url = `/${domainAliasResource}/`
|
||||
if (domain !== undefined) {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<translate class="headline">DNS configuration overview for {{ domain.name }}</translate>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<translate class="overline">MX records</translate>
|
||||
<v-data-table
|
||||
:headers="mxRecordHeaders"
|
||||
:items="detail.mx_records"
|
||||
hide-default-footer
|
||||
>
|
||||
<template v-slot:item.updated="{ item }">
|
||||
{{ item.updated|date }}
|
||||
</template>
|
||||
</v-data-table>
|
||||
<translate class="overline">Auto configuration</translate>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-if="detail.autoconfig_record"
|
||||
color="success"
|
||||
>
|
||||
<translate>autoconfig record (Mozilla): {{ detail.autoconfig_record.value }}</translate>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="warning"
|
||||
>
|
||||
<translate>autoconfig record (Mozilla) not found</translate>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-if="detail.autodiscover_record"
|
||||
color="success"
|
||||
>
|
||||
<translate>autodiscover record (Microsoft): {{ detail.autodiscover_record.value }}</translate>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="warning"
|
||||
>
|
||||
<translate>autodiscover record (Microsoft) not found</translate>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<translate class="overline">Authentication</translate>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-if="detail.spf_record"
|
||||
color="success"
|
||||
>
|
||||
<translate>SPF record found</translate>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="error"
|
||||
>
|
||||
<translate>SPF record not found</translate>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-if="detail.dkim_record"
|
||||
color="success"
|
||||
>
|
||||
<translate>DKIM record found</translate>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="error"
|
||||
>
|
||||
<translate>DKIM record not found</translate>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-if="detail.dmarc_record"
|
||||
color="success"
|
||||
>
|
||||
<translate>DMARC record found</translate>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="error"
|
||||
>
|
||||
<translate>DMARC record not found</translate>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="grey darken-1"
|
||||
text
|
||||
@click="close"
|
||||
>
|
||||
<translate>Close</translate>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import domains from '@/api/domains'
|
||||
|
||||
export default {
|
||||
props: ['domain'],
|
||||
data () {
|
||||
return {
|
||||
detail: {},
|
||||
mxRecordHeaders: [
|
||||
{ text: this.$gettext('Name'), value: 'name' },
|
||||
{ text: this.$gettext('Address'), value: 'address' },
|
||||
{ text: this.$gettext('Updated'), value: 'updated' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$emit('close')
|
||||
this.detail = {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
domain: {
|
||||
handler: function (val) {
|
||||
domains.getDomainDNSDetail(val.pk).then(resp => {
|
||||
this.detail = resp.data
|
||||
})
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,120 +1,122 @@
|
||||
<template>
|
||||
<v-card class="mt-6">
|
||||
<v-toolbar flat>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" small>
|
||||
Actions <v-icon right>mdi-chevron-down</v-icon>
|
||||
<v-card class="mt-6">
|
||||
<v-toolbar flat>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" small>
|
||||
Actions <v-icon right>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
placeholder="Search"
|
||||
filled
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-toolbar>
|
||||
<v-data-table v-model="selected"
|
||||
:headers="headers"
|
||||
:items="domains"
|
||||
:search="search"
|
||||
item-key="name"
|
||||
class="elevation-1"
|
||||
show-select
|
||||
single-expand
|
||||
:expanded.sync="expanded"
|
||||
@item-expanded="loadAliases"
|
||||
@click:row="showAliases"
|
||||
>
|
||||
<template v-slot:item="{ item, isExpanded, expand }">
|
||||
<tr>
|
||||
<td>
|
||||
<v-checkbox />
|
||||
</td>
|
||||
<td>
|
||||
<router-link :to="{ name: 'DomainDetail', params: { pk: item.pk } }">
|
||||
{{ item.name }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.domainalias_count }} aliases
|
||||
<v-btn v-if="item.domainalias_count"
|
||||
icon
|
||||
@click="expand(!isExpanded)"
|
||||
>
|
||||
<v-icon v-if="!isExpanded">mdi-chevron-down</v-icon>
|
||||
<v-icon v-else>mdi-chevron-up</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
placeholder="Search"
|
||||
filled
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-toolbar>
|
||||
<v-data-table v-model="selected"
|
||||
:headers="headers"
|
||||
:items="domains"
|
||||
:search="search"
|
||||
item-key="name"
|
||||
class="elevation-1"
|
||||
show-select
|
||||
single-expand
|
||||
:expanded.sync="expanded"
|
||||
@item-expanded="loadAliases"
|
||||
@click:row="showAliases"
|
||||
>
|
||||
<template v-slot:item="{ item, isExpanded, expand }">
|
||||
<tr>
|
||||
<td>
|
||||
<v-checkbox />
|
||||
</td>
|
||||
<td>
|
||||
<router-link :to="{ name: 'DomainDetail', params: { pk: item.pk } }">
|
||||
{{ item.name }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.domainalias_count }} aliases
|
||||
<v-btn v-if="item.domainalias_count"
|
||||
icon
|
||||
@click="expand(!isExpanded)"
|
||||
>
|
||||
<v-icon v-if="!isExpanded">mdi-chevron-down</v-icon>
|
||||
<v-icon v-else>mdi-chevron-up</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
<v-chip :color="getDNSTagType(item.dns_global_status)"
|
||||
small>
|
||||
{{ getDNSLabel(item.dns_global_status) }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>
|
||||
<v-progress-linear v-model="item.allocated_quota_in_percent" />
|
||||
</td>
|
||||
<td>
|
||||
<v-progress-linear v-model="item.allocated_quota_in_percent" />
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-right">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on">
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item :to="{ name: 'DomainEdit', params: { domainPk: item.pk }}">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-circle-edit-outline</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title><translate>Edit</translate></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteDomain(item)" color="red">
|
||||
<v-list-item-icon>
|
||||
<v-icon color="red">mdi-delete-outline</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title><translate>Delete</translate></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-slot:expanded-item="{ headers, item }">
|
||||
<tr>
|
||||
<td :colspan="headers.length">
|
||||
<v-chip v-for="alias in aliases[item.name]"
|
||||
:key="alias.name"
|
||||
class="mr-2"
|
||||
pill
|
||||
title="DNS OK"
|
||||
>
|
||||
<v-icon left color="success">mdi-server-network</v-icon>
|
||||
{{ alias.name }}
|
||||
<v-btn class="ml-4" x-small icon><v-icon>mdi-circle-edit-outline</v-icon></v-btn>
|
||||
<v-btn x-small icon color="error"><v-icon>mdi-delete-outline</v-icon></v-btn>
|
||||
</v-chip>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</td>
|
||||
<td>
|
||||
<v-chip :color="getDNSTagType(item.dns_global_status)"
|
||||
@click="openDNSDetail(item)"
|
||||
small>
|
||||
{{ getDNSLabel(item.dns_global_status) }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>
|
||||
<v-progress-linear v-model="item.allocated_quota_in_percent" />
|
||||
</td>
|
||||
<td>
|
||||
<v-progress-linear v-model="item.allocated_quota_in_percent" />
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-right">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on">
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item :to="{ name: 'DomainEdit', params: { domainPk: item.pk }}">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-circle-edit-outline</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title><translate>Edit</translate></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteDomain(item)" color="red">
|
||||
<v-list-item-icon>
|
||||
<v-icon color="red">mdi-delete-outline</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title><translate>Delete</translate></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-slot:expanded-item="{ headers, item }">
|
||||
<tr class="grey lighten-4">
|
||||
<td :colspan="headers.length">
|
||||
<span v-for="alias in aliases[item.name]"
|
||||
:key="alias.name"
|
||||
class="mr-4"
|
||||
>
|
||||
<a href="#" class="mr-2">{{ alias.name }}</a>
|
||||
<v-chip x-small color="success">DNS OK</v-chip>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<confirm-dialog ref="confirm" />
|
||||
<v-dialog v-model="showDNSdetail"
|
||||
persistent
|
||||
max-width="800px">
|
||||
<dns-detail @close="showDNSdetail = false" :domain="selectedDomain" />
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -122,10 +124,12 @@
|
||||
import { mapGetters } from 'vuex'
|
||||
import domainApi from '@/api/domains'
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog'
|
||||
import DNSDetail from '@/components/domains/DNSDetail'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmDialog
|
||||
ConfirmDialog,
|
||||
'dns-detail': DNSDetail
|
||||
},
|
||||
computed: mapGetters({
|
||||
domains: 'domains/domains'
|
||||
@@ -142,6 +146,7 @@ export default {
|
||||
],
|
||||
selectedDomain: null,
|
||||
showConfirmDialog: false,
|
||||
showDNSdetail: false,
|
||||
search: '',
|
||||
selected: [],
|
||||
expanded: [],
|
||||
@@ -205,6 +210,10 @@ export default {
|
||||
this.$set(this.aliases, item.name, resp.data)
|
||||
})
|
||||
},
|
||||
openDNSDetail (domain) {
|
||||
this.selectedDomain = domain
|
||||
this.showDNSdetail = true
|
||||
},
|
||||
showAliases (item) {
|
||||
this.expanded = [{ item }]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
app
|
||||
dark
|
||||
>
|
||||
<div class="d-flex justify-center">
|
||||
<div class="d-flex justify-start">
|
||||
<v-img
|
||||
src="../../assets/Modoboa_RVB-BLANC-SANS.png"
|
||||
max-width="190"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
app
|
||||
clipped-left
|
||||
>
|
||||
<div class="d-flex">
|
||||
<div class="d-flex justify-start">
|
||||
<v-img
|
||||
class="mx-2"
|
||||
src="../../assets/Modoboa_RVB-BLANC-SANS.png"
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import { DateTime } from 'luxon'
|
||||
import { translate } from 'vue-gettext'
|
||||
const { gettext: $gettext } = translate
|
||||
|
||||
Vue.filter('yesno', function (value) {
|
||||
return (value) ? $gettext('yes') : $gettext('no')
|
||||
})
|
||||
|
||||
/*
|
||||
** Return a human readable version of the given date string.
|
||||
*/
|
||||
Vue.filter('date', function (value) {
|
||||
return DateTime.fromISO(value).setLocale('en').toLocaleString(DateTime.DATETIME_MED)
|
||||
})
|
||||
|
||||
@@ -6180,6 +6180,11 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
luxon@^1.26.0:
|
||||
version "1.26.0"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.26.0.tgz#d3692361fda51473948252061d0f8561df02b578"
|
||||
integrity sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A==
|
||||
|
||||
magic-string@^0.25.7:
|
||||
version "0.25.7"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
|
||||
|
||||
@@ -163,11 +163,11 @@ class Domain(mixins.MessageLimitMixin, AdminObject):
|
||||
if config["enable_dnsbl_checks"] and self.dnsblresult_set.blacklisted().exists():
|
||||
errors.append("dnsbl")
|
||||
if config["enable_spf_checks"]:
|
||||
if self.spf_record is None or not self.spf_record.is_valid():
|
||||
if self.spf_record is None or not self.spf_record.is_valid:
|
||||
errors.append("spf")
|
||||
if self.dkim_record is None or not self.dkim_record.is_valid():
|
||||
if self.dkim_record is None or not self.dkim_record.is_valid:
|
||||
errors.append("dkim")
|
||||
if self.dmarc_record is None or not self.dmarc_record.is_valid():
|
||||
if self.dmarc_record is None or not self.dmarc_record.is_valid:
|
||||
errors.append("dmarc")
|
||||
if config["enable_autoconfig_checks"]:
|
||||
if self.autoconfig_record is None:
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""App related serializers."""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class MXRecordSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for MXRecord."""
|
||||
|
||||
class Meta:
|
||||
model = admin_models.MXRecord
|
||||
fields = ("name", "address", "updated")
|
||||
|
||||
|
||||
class DNSBLResultSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for DNSBLResult."""
|
||||
|
||||
mx = MXRecordSerializer()
|
||||
|
||||
class Meta:
|
||||
model = admin_models.DNSBLResult
|
||||
fields = ("provider", "mx", "status")
|
||||
|
||||
|
||||
class DNSRecordSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for DNSRecord."""
|
||||
|
||||
class Meta:
|
||||
model = models.DNSRecord
|
||||
fields = ("type", "value", "is_valid", "error", "updated")
|
||||
|
||||
|
||||
class DNSDetailSerializer(serializers.ModelSerializer):
|
||||
|
||||
mx_records = MXRecordSerializer(many=True, source="mxrecord_set")
|
||||
autoconfig_record = DNSRecordSerializer()
|
||||
autodiscover_record = DNSRecordSerializer()
|
||||
spf_record = DNSRecordSerializer()
|
||||
dkim_record = DNSRecordSerializer()
|
||||
dmarc_record = DNSRecordSerializer()
|
||||
dnsbl_results = DNSBLResultSerializer(many=True, source="dnsblresult_set")
|
||||
|
||||
class Meta:
|
||||
model = admin_models.Domain
|
||||
fields = (
|
||||
"mx_records", "dnsbl_results", "autoconfig_record",
|
||||
"autodiscover_record", "spf_record", "dkim_record", "dmarc_record"
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
"""App related API urls."""
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import viewsets
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r"domains", viewsets.DNSViewSet, basename="dns")
|
||||
|
||||
urlpatterns = router.urls
|
||||
@@ -0,0 +1,26 @@
|
||||
"""App. related viewsets."""
|
||||
|
||||
from rest_framework import permissions, response, viewsets
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
|
||||
from . import serializers
|
||||
|
||||
|
||||
class DNSViewSet(viewsets.GenericViewSet):
|
||||
"""A viewset to provide extra routes related to DNS information."""
|
||||
|
||||
permission_classes = (permissions.IsAuthenticated, )
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter queryset based on current user."""
|
||||
return admin_models.Domain.objects.get_for_admin(self.request.user)
|
||||
|
||||
@action(methods=["get"],
|
||||
detail=True,
|
||||
serializer_class=serializers.DNSDetailSerializer)
|
||||
def dns_detail(self, request, **kwargs):
|
||||
domain = self.get_object()
|
||||
serializer = self.get_serializer(domain)
|
||||
return response.Response(serializer.data)
|
||||
@@ -20,6 +20,7 @@ urlpatterns = [
|
||||
path('', include("modoboa.parameters.urls_api")),
|
||||
path('', include("modoboa.limits.urls_api")),
|
||||
path('', include("modoboa.relaydomains.urls_api")),
|
||||
path('', include("modoboa.dnstools.urls_api")),
|
||||
]
|
||||
|
||||
urlpatterns += exts_pool.get_urls(category="api")
|
||||
|
||||
Reference in New Issue
Block a user