diff --git a/frontend/src/api/admin.js b/frontend/src/api/admin.js
new file mode 100644
index 000000000..45f53e838
--- /dev/null
+++ b/frontend/src/api/admin.js
@@ -0,0 +1,7 @@
+import repository from './repository'
+
+export default {
+ getComponentsInformation () {
+ return repository.get('/admin/components')
+ }
+}
diff --git a/frontend/src/components/admin/ComponentsInformationTable.vue b/frontend/src/components/admin/ComponentsInformationTable.vue
new file mode 100644
index 000000000..84b3fda86
--- /dev/null
+++ b/frontend/src/components/admin/ComponentsInformationTable.vue
@@ -0,0 +1,72 @@
+
+
+
+ Installed components
+
+
+
+ One or more updates are available
+
+ Check out the following list to find related components
+
+
+
+
+
+ {{ item.last_version }}
+
+
+ {{ item.last_version }}
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/domains/DomainList.vue b/frontend/src/components/domains/DomainList.vue
index 1a2b7db4d..681346f0d 100644
--- a/frontend/src/components/domains/DomainList.vue
+++ b/frontend/src/components/domains/DomainList.vue
@@ -165,7 +165,7 @@ export default {
headers: [
{ text: this.$gettext('Name'), value: 'name' },
{ text: this.$gettext('Aliases'), value: 'domainalias_count' },
- { text: this.$gettext('DNS status'), value: 'dns_global_status', sortable: false, align: 'center' },
+ { text: this.$gettext('DNS status'), value: 'dns_global_status', sortable: false },
{ text: this.$gettext('Sending limit'), value: 'message_limit' },
{ text: this.$gettext('Quota'), value: 'allocated_quota_in_percent' },
{ text: this.$gettext('Actions'), value: 'actions', sortable: false, align: 'right' }
diff --git a/frontend/src/components/layout/Navbar.vue b/frontend/src/components/layout/Navbar.vue
index c7c54c5e9..4c4ddc9a7 100644
--- a/frontend/src/components/layout/Navbar.vue
+++ b/frontend/src/components/layout/Navbar.vue
@@ -201,6 +201,12 @@ export default {
text: this.$gettext('Settings'),
children: [],
roles: ['SuperAdmins']
+ },
+ {
+ icon: 'mdi-information',
+ text: this.$gettext('Information'),
+ roles: ['SuperAdmins'],
+ to: { name: 'Information' }
}
],
userSettingsMenuItems: [
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index 2f0c51e2e..49e554976 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -136,6 +136,15 @@ const routes = [
allowedRoles: ['DomainAdmins', 'SuperAdmins']
}
},
+ {
+ path: '/information',
+ name: 'Information',
+ component: () => import('../views/admin/Information.vue'),
+ meta: {
+ requiresAuth: true,
+ allowedRoles: ['SuperAdmins']
+ }
+ },
{
path: '/user/api',
name: 'APISetup',
diff --git a/frontend/src/views/admin/Information.vue b/frontend/src/views/admin/Information.vue
new file mode 100644
index 000000000..e088b62eb
--- /dev/null
+++ b/frontend/src/views/admin/Information.vue
@@ -0,0 +1,19 @@
+
+
+
+ Information
+
+
+
+
+
+
+
diff --git a/frontend/src/views/user/PasswordRecoveryForm.vue b/frontend/src/views/user/PasswordRecoveryForm.vue
index 2aecb0607..516991229 100644
--- a/frontend/src/views/user/PasswordRecoveryForm.vue
+++ b/frontend/src/views/user/PasswordRecoveryForm.vue
@@ -35,7 +35,7 @@
diff --git a/modoboa/core/api/v2/serializers.py b/modoboa/core/api/v2/serializers.py
index d38123777..7afaff798 100644
--- a/modoboa/core/api/v2/serializers.py
+++ b/modoboa/core/api/v2/serializers.py
@@ -468,3 +468,15 @@ class PasswordRecoveryConfirmSerializer(serializers.Serializer):
user.set_password(self.validated_data["new_password1"])
user.save()
return True
+
+
+class ModoboaComponentSerializer(serializers.Serializer):
+ """Serializer used for information endpoint."""
+
+ label = serializers.CharField()
+ name = serializers.CharField()
+ version = serializers.CharField()
+ last_version = serializers.CharField(required=False)
+ description = serializers.CharField()
+ update = serializers.BooleanField(default=False)
+ changelog_url = serializers.URLField(required=False)
diff --git a/modoboa/core/api/v2/tests.py b/modoboa/core/api/v2/tests.py
index 6c72b102f..ed6bdd5e4 100644
--- a/modoboa/core/api/v2/tests.py
+++ b/modoboa/core/api/v2/tests.py
@@ -441,3 +441,11 @@ class LanguageViewSetTestCase(ModoAPITestCase):
url = reverse("v2:language-list")
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
+
+
+class ComponentAPITestCase(ModoAPITestCase):
+
+ def test_information(self):
+ url = reverse("v2:components_information")
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, 200)
diff --git a/modoboa/core/api/v2/urls.py b/modoboa/core/api/v2/urls.py
index 3435412ad..da5784b20 100644
--- a/modoboa/core/api/v2/urls.py
+++ b/modoboa/core/api/v2/urls.py
@@ -28,4 +28,6 @@ urlpatterns += [
name="password_reset_confirm_v2"),
path('sms_totp/', views.PasswordResetSmsTOTP.as_view(),
name="sms_totp"),
+ path('admin/components/', views.ComponentsInformationAPIView.as_view(),
+ name="components_information"),
]
diff --git a/modoboa/core/api/v2/views.py b/modoboa/core/api/v2/views.py
index ef07211af..7366e3161 100644
--- a/modoboa/core/api/v2/views.py
+++ b/modoboa/core/api/v2/views.py
@@ -8,12 +8,14 @@ from django.utils.datastructures import MultiValueDictKeyError
from django.contrib.auth import login
+from drf_spectacular.utils import extend_schema
from rest_framework import response, status
from rest_framework_simplejwt import views as jwt_views
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from rest_framework.views import APIView
from modoboa.core.password_hashers import get_password_hasher
+from modoboa.core.utils import check_for_updates
from modoboa.parameters import tools as param_tools
from smtplib import SMTPException
@@ -145,7 +147,8 @@ class PasswordResetConfirmView(APIView):
""" Get and set new user password. """
def post(self, request, *args, **kwargs):
- serializer = serializers.PasswordRecoveryConfirmSerializer(data=request.data)
+ serializer = serializers.PasswordRecoveryConfirmSerializer(
+ data=request.data)
try:
serializer.is_valid(raise_exception=True)
except serializers.PasswordRequirementsFailure as e:
@@ -157,3 +160,15 @@ class PasswordResetConfirmView(APIView):
return response.Response(data, 400)
serializer.save()
return response.Response(status=200)
+
+
+class ComponentsInformationAPIView(APIView):
+ """Retrieve information about installed components."""
+
+ @extend_schema(responses=serializers.ModoboaComponentSerializer(many=True))
+ def get(self, request, *args, **kwargs):
+ status, extensions = check_for_updates()
+ serializer = serializers.ModoboaComponentSerializer(
+ extensions, many=True
+ )
+ return response.Response(serializer.data)
diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py
index a48d54045..7b4aaeee1 100644
--- a/modoboa/core/app_settings.py
+++ b/modoboa/core/app_settings.py
@@ -528,10 +528,11 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
self._add_visibilty_rules(
sms_backends.get_all_backend_visibility_rules()
)
+ available_schemes = get_dovecot_schemes()
self.fields["password_scheme"].choices = [
(hasher.name, ugettext_lazy(hasher.label))
for hasher in PasswordHasher.get_password_hashers()
- if hasher().scheme in get_dovecot_schemes()
+ if hasher().scheme in available_schemes
]
def _add_dynamic_fields(self):