mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
reworked alarms, added alarm on dkim issue
This commit is contained in:
+29
-1
@@ -35,6 +35,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import alarms from '@/api/alarms'
|
||||
import { bus } from '@/main'
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
|
||||
@@ -51,12 +52,21 @@ export default {
|
||||
created () {
|
||||
// this.$store.dispatch('auth/initialize')
|
||||
bus.$on('notification', this.showNotification)
|
||||
bus.$on('logged', this.checkAlarms)
|
||||
},
|
||||
mounted () {
|
||||
this.checkAlarms()
|
||||
},
|
||||
beforeDestroy () {
|
||||
clearInterval(this.alarmChecker)
|
||||
},
|
||||
data: () => ({
|
||||
snackbar: false,
|
||||
notification: '',
|
||||
notificationColor: 'success',
|
||||
notificationTimeout: 2000
|
||||
notificationTimeout: 2000,
|
||||
alarmNotified: [],
|
||||
alarmChecker: null
|
||||
}),
|
||||
methods: {
|
||||
showNotification (options) {
|
||||
@@ -65,6 +75,24 @@ export default {
|
||||
this.notificationColor = (options.type) ? options.type : 'success'
|
||||
this.snackbar = true
|
||||
}
|
||||
},
|
||||
checkAlarms () {
|
||||
if (this.isAuthenticated) {
|
||||
this.alarmChecker = window.setInterval(() => {
|
||||
alarms.getAll({}).then(resp => {
|
||||
let count = 0
|
||||
for (const alarm of resp.data) {
|
||||
if (alarm.status === 1 && !this.alarmNotified.includes(alarm.id)) {
|
||||
count++
|
||||
this.alarmNotified.push(alarm.id)
|
||||
}
|
||||
}
|
||||
if (count !== 0) {
|
||||
bus.$emit('notification', { msg: this.$gettext('You have one or more opened alarms'), type: 'error' })
|
||||
}
|
||||
})
|
||||
}, 10000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,11 @@ const resource = 'alarms'
|
||||
export default {
|
||||
getAll (params) {
|
||||
return repository.get(`${resource}/`, { params })
|
||||
},
|
||||
switchStatus (alarmId, data) {
|
||||
return repository.patch(`${resource}/${alarmId}/switch/`, data)
|
||||
},
|
||||
delete (alarmId) {
|
||||
return repository.delete(`${resource}/${alarmId}/`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<menu-items :items="getActionMenuItems()"/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -26,7 +27,7 @@
|
||||
:headers="headers"
|
||||
:items="alarms"
|
||||
:search="search"
|
||||
item-key="pk"
|
||||
item-key="id"
|
||||
:options.sync="options"
|
||||
:server-items-length="totalAlarms"
|
||||
:loading="loading"
|
||||
@@ -53,18 +54,39 @@
|
||||
<template v-if="item.mailbox">
|
||||
{{ item.mailbox.address }}@{{ item.domain.name }}
|
||||
</template>
|
||||
<template v-else>
|
||||
/
|
||||
</template>
|
||||
</template>
|
||||
<template v-slot:item.created="{ item }">
|
||||
{{ item.created|date }}
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<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>
|
||||
<menu-items :items="getMenuItems(item)" :object="item" />
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import alarms from '@/api/alarms'
|
||||
import debounce from 'debounce'
|
||||
import MenuItems from '@/components/tools/MenuItems'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuItems
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
alarms: [],
|
||||
@@ -73,7 +95,8 @@ export default {
|
||||
{ text: this.$gettext('Status'), value: 'status' },
|
||||
{ text: this.$gettext('Domain'), value: 'domain.name', sortable: false },
|
||||
{ text: this.$gettext('Mailbox'), value: 'mailbox', sortable: false },
|
||||
{ text: this.$gettext('Message'), value: 'title' }
|
||||
{ text: this.$gettext('Message'), value: 'title' },
|
||||
{ text: this.$gettext('Actions'), value: 'actions', sortable: false, align: 'right' }
|
||||
],
|
||||
loading: true,
|
||||
options: {},
|
||||
@@ -83,6 +106,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sleep (milliseconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds))
|
||||
},
|
||||
fetchAlarms () {
|
||||
const params = {
|
||||
page: this.options.page
|
||||
@@ -95,10 +121,57 @@ export default {
|
||||
}
|
||||
this.loading = true
|
||||
alarms.getAll(params).then(resp => {
|
||||
this.alarms = resp.data.results
|
||||
this.alarms = resp.data
|
||||
this.totalAlarms = resp.data.count
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
async deleteAlarm (alarm, single = true) {
|
||||
alarms.delete(alarm.id).then(() => {
|
||||
if (single) {
|
||||
bus.$emit('notification', { msg: this.$gettext('Alarm deleted') })
|
||||
this.fetchAlarms()
|
||||
}
|
||||
})
|
||||
},
|
||||
async closeAlarm (alarm) {
|
||||
alarms.switchStatus(alarm.id, { status: 2 }).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Alarm closed') })
|
||||
this.fetchAlarms()
|
||||
})
|
||||
},
|
||||
async openAlarm (alarm) {
|
||||
alarms.switchStatus(alarm.id, { status: 1 }).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Alarm re-opened') })
|
||||
this.fetchAlarms()
|
||||
})
|
||||
},
|
||||
async deleteAlarms () {
|
||||
for (const alarm of this.selected) {
|
||||
this.deleteAlarm(alarm, false)
|
||||
await this.sleep(100)
|
||||
}
|
||||
bus.$emit('notification', { msg: this.$gettext('Alarms deleted') })
|
||||
this.selected = []
|
||||
this.fetchAlarms()
|
||||
},
|
||||
getMenuItems (item) {
|
||||
const result = []
|
||||
result.push({ label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteAlarm, color: 'red' })
|
||||
if (item.status === 1) {
|
||||
result.push({ label: this.$gettext('Close alarm'), icon: 'mdi-check', onClick: this.closeAlarm, color: 'green' })
|
||||
} else {
|
||||
result.push({ label: this.$gettext('Reopen alarm'), icon: 'mdi-alert-circle-outline', onClick: this.openAlarm, color: 'orange' })
|
||||
}
|
||||
return result
|
||||
},
|
||||
getActionMenuItems () {
|
||||
const result = []
|
||||
if (this.selected.length > 0) {
|
||||
result.push({ label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteAlarms, color: 'red' })
|
||||
}
|
||||
result.push({ label: 'Reload', icon: 'mdi-reload', onClick: this.fetchAlarms })
|
||||
return result
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import Cookies from 'js-cookie'
|
||||
import auth from '@/api/auth'
|
||||
|
||||
@@ -86,6 +87,7 @@ export default {
|
||||
Cookies.set('token', resp.data.access, { sameSite: 'strict' })
|
||||
Cookies.set('refreshToken', resp.data.refresh, { sameSite: 'strict' })
|
||||
this.$store.dispatch('auth/initialize').then(() => {
|
||||
bus.$emit('logged')
|
||||
this.$router.push({ name: 'DomainList' })
|
||||
})
|
||||
}).catch(err => {
|
||||
|
||||
@@ -5,7 +5,7 @@ module.exports = {
|
||||
target:
|
||||
process.env.DOCKER == "yes"
|
||||
? "http://api:8000/"
|
||||
: "http://localhost:8000",
|
||||
: "http://127.0.0.1:8000",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -225,6 +225,10 @@ class AdminGlobalParametersSerializer(serializers.Serializer):
|
||||
raise serializers.ValidationError(
|
||||
_("Directory not found.")
|
||||
)
|
||||
if not os.access(value, os.W_OK):
|
||||
raise serializers.ValidationError(
|
||||
_("Directory non-writable")
|
||||
)
|
||||
code, output = exec_cmd("which openssl")
|
||||
if code:
|
||||
raise serializers.ValidationError(
|
||||
@@ -588,3 +592,14 @@ class AlarmSerializer(serializers.ModelSerializer):
|
||||
depth = 1
|
||||
fields = "__all__"
|
||||
model = models.Alarm
|
||||
|
||||
|
||||
class AlarmSwitchStatusSerializer(serializers.Serializer):
|
||||
"""Serializer to switch the status of an Alarm."""
|
||||
|
||||
status = serializers.IntegerField()
|
||||
|
||||
def validate(self, data):
|
||||
if data["status"] not in [constants.ALARM_OPENED, constants.ALARM_CLOSED]:
|
||||
raise ValidationError(_("Unknown status"))
|
||||
return data
|
||||
|
||||
@@ -10,7 +10,7 @@ from rest_framework import (
|
||||
filters, mixins, parsers, pagination, permissions, response, status, viewsets
|
||||
)
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
|
||||
from modoboa.admin.api.v1 import viewsets as v1_viewsets
|
||||
from modoboa.core import models as core_models
|
||||
@@ -20,6 +20,7 @@ from modoboa.lib.throttle import GetThrottleViewsetMixin
|
||||
|
||||
from ... import lib
|
||||
from ... import models
|
||||
from ... import constants
|
||||
from . import serializers
|
||||
|
||||
|
||||
@@ -325,7 +326,10 @@ class UserAccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
|
||||
return response.Response(serializer.validated_data)
|
||||
|
||||
|
||||
class AlarmViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class AlarmViewSet(GetThrottleViewsetMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
"""Viewset for Alarm."""
|
||||
|
||||
filter_backends = (filters.OrderingFilter, filters.SearchFilter, )
|
||||
@@ -342,3 +346,35 @@ class AlarmViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
domain__in=models.Domain.objects.get_for_admin(
|
||||
self.request.user)
|
||||
).order_by("-created")
|
||||
|
||||
@action(methods=["delete"], detail=True)
|
||||
def delete(self, request, **kwargs):
|
||||
"""Custom delete method that accepts body arguments."""
|
||||
alarm = self.get_object()
|
||||
domain = models.Domain.objects.filter(pk=alarm.domain.pk)
|
||||
if not domain.exists():
|
||||
alarm.delete()
|
||||
if not request.user.can_access(domain):
|
||||
raise PermissionDenied(_("You don't have access to this domain"))
|
||||
else:
|
||||
alarm.delete()
|
||||
return response.Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(methods=['patch'], detail=True)
|
||||
def switch(self, request, **kwargs):
|
||||
"""Custom update method that switch status of an alarm."""
|
||||
alarm = self.get_object()
|
||||
serializer = serializers.AlarmSwitchStatusSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
domain = models.Domain.objects.filter(pk=alarm.domain.pk)
|
||||
if not domain.exists():
|
||||
alarm.delete()
|
||||
raise ValidationError(_("No alarm found"))
|
||||
if not request.user.can_access(domain):
|
||||
raise PermissionDenied(_("You don't have access to this domain"))
|
||||
else:
|
||||
if serializer.data["status"] == constants.ALARM_CLOSED:
|
||||
alarm.close()
|
||||
else:
|
||||
alarm.reopen()
|
||||
return response.Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -35,6 +35,8 @@ DKIM_KEY_LENGTHS = [
|
||||
(4096, "4096"),
|
||||
]
|
||||
|
||||
DKIM_WRITE_ERROR = "DKIM path error"
|
||||
|
||||
ALARM_OPENED = 1
|
||||
ALARM_CLOSED = 2
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ import os
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.lib import sysutils
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from .... import models
|
||||
from .... import signals
|
||||
from ....constants import DKIM_WRITE_ERROR, ALARM_OPENED
|
||||
|
||||
|
||||
class ManageDKIMKeys(BaseCommand):
|
||||
@@ -19,6 +21,25 @@ class ManageDKIMKeys(BaseCommand):
|
||||
"""Create a new DKIM key."""
|
||||
storage_dir = param_tools.get_global_parameter("dkim_keys_storage_dir")
|
||||
pkey_path = os.path.join(storage_dir, "{}.pem".format(domain.name))
|
||||
alarm_qset = models.alarm.Alarm.objects.filter(
|
||||
domain=domain.pk,
|
||||
internal_name=DKIM_WRITE_ERROR)
|
||||
if not os.access(storage_dir, os.W_OK):
|
||||
if not alarm_qset:
|
||||
alarm = models.alarm.Alarm.objects.create(
|
||||
domain=domain,
|
||||
title=_("DKIM path non-writable"),
|
||||
internal_name=DKIM_WRITE_ERROR
|
||||
)
|
||||
alarm.save()
|
||||
else:
|
||||
alarm = alarm_qset.first()
|
||||
if alarm.status != ALARM_OPENED:
|
||||
alarm.status = ALARM_OPENED
|
||||
alarm.save()
|
||||
return
|
||||
elif alarm_qset:
|
||||
alarm_qset.first().close()
|
||||
key_size = (
|
||||
domain.dkim_key_length if domain.dkim_key_length
|
||||
else self.default_key_length)
|
||||
|
||||
@@ -51,5 +51,11 @@ class Alarm(models.Model):
|
||||
self.closed = timezone.now()
|
||||
self.save()
|
||||
|
||||
def reopen(self):
|
||||
"""Reopen this alarm."""
|
||||
self.status = constants.ALARM_OPENED
|
||||
self.closed = None
|
||||
self.save()
|
||||
|
||||
|
||||
reversion.register(Alarm)
|
||||
|
||||
Reference in New Issue
Block a user