Added DNS Detail page.

This commit is contained in:
Antoine Nguyen
2021-02-16 19:14:10 +01:00
parent 9cd856cf03
commit d564aae5f3
13 changed files with 373 additions and 120 deletions
+1
View File
@@ -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",
+3
View File
@@ -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>
+124 -115
View File
@@ -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 }]
}
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+8
View File
@@ -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)
})
+5
View File
@@ -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"
+3 -3
View File
@@ -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:
+51
View File
@@ -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"
)
+10
View File
@@ -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
+26
View File
@@ -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)
+1
View File
@@ -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")