diff --git a/frontend/package.json b/frontend/package.json index 5ae1be49c..090e3cca7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,10 +41,10 @@ "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.0", "eslint-plugin-vue": "^6.2.2", - "sass": "^1.26.5", + "sass": "~1.32.12", "sass-loader": "^8.0.2", "vue-cli-plugin-axios": "~0.0.4", - "vue-cli-plugin-vuetify": "~2.0.7", + "vue-cli-plugin-vuetify": "~2.4.1", "vue-template-compiler": "^2.6.11", "vuetify-loader": "^1.3.0" } diff --git a/frontend/src/api/logs.js b/frontend/src/api/logs.js index a2f108cb2..3949df695 100644 --- a/frontend/src/api/logs.js +++ b/frontend/src/api/logs.js @@ -3,7 +3,10 @@ import repository from './repository' const resource = 'logs' export default { - getAll (params) { - return repository.get(`/${resource}/`, { params }) + getAuditTrail (params) { + return repository.get(`/${resource}/audit-trail/`, { params }) + }, + getMessages (params) { + return repository.get(`/${resource}/messages/`, { params }) } } diff --git a/frontend/src/components/layout/Navbar.vue b/frontend/src/components/layout/Navbar.vue index c56b8f702..76fe795b2 100644 --- a/frontend/src/components/layout/Navbar.vue +++ b/frontend/src/components/layout/Navbar.vue @@ -152,8 +152,19 @@ export default { { icon: 'mdi-history', text: this.$gettext('Logs'), - to: { name: 'AuditTrail' }, - roles: ['SuperAdmins'] + roles: ['SuperAdmins', 'Resellers', 'DomainAdmins'], + children: [ + { + text: this.$gettext('Audit trail'), + to: { name: 'AuditTrail' }, + roles: ['SuperAdmins'] + }, + { + text: this.$gettext('Messages'), + to: { name: 'MessageLog' }, + roles: ['DomainAdmins', 'Resellers', 'SuperAdmins'] + } + ] }, { icon: 'mdi-cog', diff --git a/frontend/src/components/logs/AuditTrailList.vue b/frontend/src/components/logs/AuditTrailList.vue index a8088c068..c02efbbc1 100644 --- a/frontend/src/components/logs/AuditTrailList.vue +++ b/frontend/src/components/logs/AuditTrailList.vue @@ -37,10 +37,10 @@ export default { return { logs: [], headers: [ - { text: 'Date', value: 'date_created' }, - { text: 'Level', value: 'level' }, - { text: 'Logger', value: 'logger' }, - { text: 'Message', value: 'message' } + { text: this.$gettext('Date'), value: 'date_created' }, + { text: this.$gettext('Level'), value: 'level' }, + { text: this.$gettext('Logger'), value: 'logger' }, + { text: this.$gettext('Message'), value: 'message' } ], loading: true, options: {}, @@ -56,7 +56,7 @@ export default { if (level === 'WARNING') { return 'orange' } - if (level === 'CRITICAL') { + if (level === 'CRITICAL' || level === 'ERROR') { return 'red' } return '' @@ -73,7 +73,7 @@ export default { search: this.search } this.loading = true - logs.getAll(params).then(response => { + logs.getAuditTrail(params).then(response => { this.logs = response.data.results this.total = response.data.count this.loading = false diff --git a/frontend/src/components/logs/MessageList.vue b/frontend/src/components/logs/MessageList.vue new file mode 100644 index 000000000..273a00a43 --- /dev/null +++ b/frontend/src/components/logs/MessageList.vue @@ -0,0 +1,85 @@ + + + diff --git a/frontend/src/filters/index.js b/frontend/src/filters/index.js index 34bc60294..34fd40abe 100644 --- a/frontend/src/filters/index.js +++ b/frontend/src/filters/index.js @@ -13,3 +13,11 @@ Vue.filter('yesno', function (value) { Vue.filter('date', function (value) { return DateTime.fromISO(value).setLocale('en').toLocaleString(DateTime.DATETIME_MED) }) + +/* +** Truncate given string. +*/ +Vue.filter('truncate', function (text, length, clamp) { + clamp = clamp || '...' + return text.length > length ? text.slice(0, length) + clamp : text +}) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 4ff2f90dc..651c46202 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -93,13 +93,22 @@ const routes = [ } }, { - path: '/logs/', + path: '/logs/audit_trail', name: 'AuditTrail', component: () => import('../views/logs/AuditTrail.vue'), meta: { requiresAuth: true, allowedRoles: ['SuperAdmins'] } + }, + { + path: '/logs/messages', + name: 'MessageLog', + component: () => import('../views/logs/Messages.vue'), + meta: { + requiresAuth: true, + allowedRoles: ['DomainAdmins', 'SuperAdmins'] + } } ] diff --git a/frontend/src/views/logs/Messages.vue b/frontend/src/views/logs/Messages.vue new file mode 100644 index 000000000..aa012226b --- /dev/null +++ b/frontend/src/views/logs/Messages.vue @@ -0,0 +1,18 @@ + + + diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 436be592b..389527d91 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.14.5": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== @@ -35,7 +35,7 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.11.5", "@babel/generator@^7.14.5": +"@babel/generator@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.5.tgz#848d7b9f031caca9d0cd0af01b063f226f52d785" integrity sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA== @@ -110,7 +110,7 @@ dependencies: "@babel/types" "^7.14.5" -"@babel/helper-function-name@^7.10.4", "@babel/helper-function-name@^7.14.5": +"@babel/helper-function-name@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz#89e2c474972f15d8e233b52ee8c480e2cfcd50c4" integrity sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ== @@ -206,14 +206,14 @@ dependencies: "@babel/types" "^7.14.5" -"@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.14.5": +"@babel/helper-split-export-declaration@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz#22b23a54ef51c2b7605d851930c1976dd0bc693a" integrity sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA== dependencies: "@babel/types" "^7.14.5" -"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.14.5": +"@babel/helper-validator-identifier@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== @@ -251,16 +251,11 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.11.5", "@babel/parser@^7.12.0", "@babel/parser@^7.13.9", "@babel/parser@^7.14.5", "@babel/parser@^7.14.6", "@babel/parser@^7.14.7", "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": +"@babel/parser@^7.12.0", "@babel/parser@^7.13.9", "@babel/parser@^7.14.5", "@babel/parser@^7.14.6", "@babel/parser@^7.14.7", "@babel/parser@^7.6.0", "@babel/parser@^7.7.0", "@babel/parser@^7.9.6": version "7.14.7" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.7.tgz#6099720c8839ca865a2637e6c85852ead0bdb595" integrity sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA== -"@babel/parser@^7.7.0": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" - integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.14.5.tgz#4b467302e1548ed3b1be43beae2cc9cf45e0bb7e" @@ -893,7 +888,7 @@ "@babel/parser" "^7.14.5" "@babel/types" "^7.14.5" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.5": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.7.0": version "7.14.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.7.tgz#64007c9774cfdc3abd23b0780bc18a3ce3631753" integrity sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ== @@ -908,22 +903,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.7.0": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" - integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.5" - "@babel/types" "^7.11.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/types@^7.0.0", "@babel/types@^7.11.5", "@babel/types@^7.12.0", "@babel/types@^7.13.0", "@babel/types@^7.14.5", "@babel/types@^7.4.4", "@babel/types@^7.6.1", "@babel/types@^7.9.6": +"@babel/types@^7.0.0", "@babel/types@^7.12.0", "@babel/types@^7.13.0", "@babel/types@^7.14.5", "@babel/types@^7.4.4", "@babel/types@^7.6.1", "@babel/types@^7.7.0", "@babel/types@^7.9.6": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.5.tgz#3bb997ba829a2104cedb20689c4a5b8121d383ff" integrity sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg== @@ -931,15 +911,6 @@ "@babel/helper-validator-identifier" "^7.14.5" to-fast-properties "^2.0.0" -"@babel/types@^7.7.0": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" - integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" - to-fast-properties "^2.0.0" - "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -8244,7 +8215,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.15.1, resolve@^1.20.0: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.15.1, resolve@^1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -8252,7 +8223,7 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.15 is-core-module "^2.2.0" path-parse "^1.0.6" -resolve@^1.10.1, resolve@^1.12.0: +resolve@^1.10.1: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -8381,10 +8352,10 @@ sass-loader@^8.0.2: schema-utils "^2.6.1" semver "^6.3.0" -sass@^1.26.5: - version "1.35.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.35.1.tgz#90ecf774dfe68f07b6193077e3b42fb154b9e1cd" - integrity sha512-oCisuQJstxMcacOPmxLNiLlj4cUyN2+8xJnG7VanRoh2GOLr9RqkvI4AxA4a6LHVg/rsu+PmxXeGhrdSF9jCiQ== +sass@~1.32.12: + version "1.32.13" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.13.tgz#8d29c849e625a415bce71609c7cf95e15f74ed00" + integrity sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA== dependencies: chokidar ">=3.0.0 <4.0.0" @@ -9692,10 +9663,10 @@ vue-cli-plugin-axios@~0.0.4: resolved "https://registry.yarnpkg.com/vue-cli-plugin-axios/-/vue-cli-plugin-axios-0.0.4.tgz#29d4eb48275c7fe15b92e1fd5d95fbe2a966436f" integrity sha512-p2b/fvPJuPBnvU8027PAAuU5DiOzUn2lku8XLG/f6c8FU0N+/MXWZAlOuHhqd9e7+KIZitwe/c8qlmv7TglbTg== -vue-cli-plugin-vuetify@~2.0.7: - version "2.0.9" - resolved "https://registry.yarnpkg.com/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.9.tgz#982991aec28e79d1f0c2eb8f6e3d62e042087ee8" - integrity sha512-J4fzpz27OmCCAA3CI56ulYsUrZ859dQAh58Z9XZilY03kd/M+svLlPkK45cBIrGGfjSqQ40oyWezA3NiPBEG8g== +vue-cli-plugin-vuetify@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.4.1.tgz#a31a9ddb274c8e512aeca7dc2f29d828a18df0af" + integrity sha512-ZfhvQ13X41atsCSKdSGiHnSJlhAccv4QIFOZmd8kyq6NktpeeWlvQz/dEKBf6u1AWKmKdwiCDuxS2VNT9fxhOA== dependencies: null-loader "^3.0.0" semver "^7.1.2" diff --git a/modoboa/core/api/v2/urls.py b/modoboa/core/api/v2/urls.py index da72ddfed..fbe9c09e2 100644 --- a/modoboa/core/api/v2/urls.py +++ b/modoboa/core/api/v2/urls.py @@ -13,7 +13,7 @@ from . import viewsets router = routers.SimpleRouter() router.register(r"account", viewsets.AccountViewSet, basename="account") -router.register(r"logs", viewsets.LogViewSet) +router.register(r"logs/audit-trail", viewsets.LogViewSet) urlpatterns = router.urls urlpatterns += [ diff --git a/modoboa/core/api/v2/viewsets.py b/modoboa/core/api/v2/viewsets.py index a2060415a..1c56f5672 100644 --- a/modoboa/core/api/v2/viewsets.py +++ b/modoboa/core/api/v2/viewsets.py @@ -1,12 +1,13 @@ """Core API v2 viewsets.""" from drf_spectacular.utils import extend_schema -from rest_framework import filters, pagination, permissions, response, viewsets +from rest_framework import filters, permissions, response, viewsets from rest_framework.decorators import action from rest_framework_simplejwt.tokens import RefreshToken from modoboa.admin.api.v1 import serializers as admin_v1_serializers from modoboa.core.api.v1 import viewsets as core_v1_viewsets +from modoboa.lib import pagination from ... import constants from ... import models @@ -38,17 +39,13 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet): }) -class CustomPageNumberPagination(pagination.PageNumberPagination): - page_size_query_param = "page_size" - - class LogViewSet(viewsets.ReadOnlyModelViewSet): """Log viewset.""" filter_backends = [filters.OrderingFilter, filters.SearchFilter] ordering = ["-date_created"] ordering_fields = "__all__" - pagination_class = CustomPageNumberPagination + pagination_class = pagination.CustomPageNumberPagination permission_classes = ( permissions.IsAuthenticated, permissions.DjangoModelPermissions, diff --git a/modoboa/lib/pagination.py b/modoboa/lib/pagination.py new file mode 100644 index 000000000..b5283358d --- /dev/null +++ b/modoboa/lib/pagination.py @@ -0,0 +1,7 @@ +"""Pagination utilities.""" + +from rest_framework import pagination + + +class CustomPageNumberPagination(pagination.PageNumberPagination): + page_size_query_param = "page_size" diff --git a/modoboa/maillog/api/v2/serializers.py b/modoboa/maillog/api/v2/serializers.py index d6ea07c18..f6c204d76 100644 --- a/modoboa/maillog/api/v2/serializers.py +++ b/modoboa/maillog/api/v2/serializers.py @@ -4,6 +4,8 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers +from ... import models + PERIODS = [ ("day", "Day"), @@ -62,3 +64,14 @@ class StatisticsSerializer(serializers.Serializer): """Serializer to return statistics.""" graphs = GraphSerializer(many=True) + + +class MaillogSerializer(serializers.ModelSerializer): + """Serializer for Maillog model.""" + + class Meta: + fields = ( + "id", "queue_id", "date", "sender", "rcpt", "original_rcpt", + "size", "status" + ) + model = models.Maillog diff --git a/modoboa/maillog/api/v2/tests.py b/modoboa/maillog/api/v2/tests.py index 273c6d726..767716a3a 100644 --- a/modoboa/maillog/api/v2/tests.py +++ b/modoboa/maillog/api/v2/tests.py @@ -23,3 +23,11 @@ class StatisticsViewSetTestCase(ModoAPITestCase): "?gset=mailtraffic&period=custom&start=2021-05-01&end=2021-05-02" ) self.assertEqual(resp.status_code, 200) + + +class MaillogViewSetTestCase(ModoAPITestCase): + + def test_list(self): + url = reverse("v2:maillog-list") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) diff --git a/modoboa/maillog/api/v2/urls.py b/modoboa/maillog/api/v2/urls.py index a8f43763f..3fd2217c2 100644 --- a/modoboa/maillog/api/v2/urls.py +++ b/modoboa/maillog/api/v2/urls.py @@ -7,5 +7,7 @@ from . import viewsets router = routers.SimpleRouter() router.register( r"statistics", viewsets.StatisticsViewSet, basename="statistics") +router.register( + r"logs/messages", viewsets.MaillogViewSet, basename="maillog") urlpatterns = router.urls diff --git a/modoboa/maillog/api/v2/viewsets.py b/modoboa/maillog/api/v2/viewsets.py index 7e1a8f423..b5bb1be8f 100644 --- a/modoboa/maillog/api/v2/viewsets.py +++ b/modoboa/maillog/api/v2/viewsets.py @@ -2,9 +2,15 @@ import time -from drf_spectacular.utils import extend_schema -from rest_framework import permissions, response, viewsets +from django.db.models import Q +from drf_spectacular.utils import extend_schema +from rest_framework import filters, permissions, response, viewsets + +from modoboa.admin import models as admin_models +from modoboa.lib import pagination + +from ... import models from ... import signals from . import serializers @@ -43,3 +49,22 @@ class StatisticsViewSet(viewsets.ViewSet): fname, start, end, serializer.validated_data.get("graphic") ) return response.Response({"graphs": graphs}) + + +class MaillogViewSet(viewsets.ReadOnlyModelViewSet): + """Simple viewset to access message log.""" + + filter_backends = [filters.OrderingFilter, filters.SearchFilter] + ordering = ["-date"] + ordering_fields = "__all__" + pagination_class = pagination.CustomPageNumberPagination + permissions = (permissions.IsAuthenticated, ) + search_fields = ["queue_id", "sender", "rcpt", "original_rcpt", "status"] + serializer_class = serializers.MaillogSerializer + + def get_queryset(self): + """Filter queryset based on current user.""" + domains = admin_models.Domain.objects.get_for_admin(self.request.user) + return models.Maillog.objects.filter( + Q(from_domain__in=domains) | Q(to_domain__in=domains) + )