From 1e64d439118d8927e678663dadfde2aa638dfcb3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 19 Sep 2025 18:15:56 +0200 Subject: [PATCH 1/3] Faster frontend authentication. --- frontend/src/router/index.js | 3 --- frontend/src/stores/auth.store.js | 17 ++++------------- frontend/src/views/login/LoginCallbackView.vue | 6 +----- modoboa/core/commands/templates/settings.py.tpl | 1 - test_project/test_project/settings.py | 1 - 5 files changed, 5 insertions(+), 23 deletions(-) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index d745f7c30..c8afeb1f5 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -490,9 +490,6 @@ router.beforeEach(async (to, from, next) => { const previousPage = window.location.href sessionStorage.setItem('previousPage', previousPage) const authStore = useAuthStore() - if (!authStore.authUser) { - await authStore.initialize() - } const isAuth = await authStore.validateAccess() if (isAuth) { if (to.meta.allowedRoles !== undefined) { diff --git a/frontend/src/stores/auth.store.js b/frontend/src/stores/auth.store.js index 5bd4899f8..275a1b9c8 100644 --- a/frontend/src/stores/auth.store.js +++ b/frontend/src/stores/auth.store.js @@ -106,17 +106,6 @@ export const useAuthStore = defineStore('auth', () => { }) } - async function initialize() { - if (isAuthenticated.value) { - return null - } - const user = await manager.getUser() - if (!user) { - return null - } - return fetchUser() - } - async function validateAccess() { const user = await manager.getUser() if (!user || user.expired) { @@ -124,6 +113,9 @@ export const useAuthStore = defineStore('auth', () => { } repository.defaults.headers.common.Authorization = `Bearer ${user.access_token}` repository.defaults.headers.post['Content-Type'] = 'application/json' + if (!isAuthenticated.value) { + await fetchUser() + } return true } @@ -137,7 +129,7 @@ export const useAuthStore = defineStore('auth', () => { async function completeLogin(redirectUrl) { try { - const user = await manager.signinRedirectCallback() + const user = await manager.signinCallback() isAuthenticated.value = true const previousPage = sessionStorage.getItem('previousPage') // Redirect the user to the previous page if available @@ -204,7 +196,6 @@ export const useAuthStore = defineStore('auth', () => { addFidoCred, deleteFidoCreds, editFidoCred, - initialize, login, $reset, updateAccount, diff --git a/frontend/src/views/login/LoginCallbackView.vue b/frontend/src/views/login/LoginCallbackView.vue index ca47858be..c3d8e7f07 100644 --- a/frontend/src/views/login/LoginCallbackView.vue +++ b/frontend/src/views/login/LoginCallbackView.vue @@ -1,5 +1,4 @@ diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index 8eb4cf226..4b21ab032 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -235,7 +235,6 @@ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', 'rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index 3a9c79627..545f28f08 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -252,7 +252,6 @@ REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "oauth2_provider.contrib.rest_framework.OAuth2Authentication", "rest_framework.authentication.TokenAuthentication", - "rest_framework.authentication.SessionAuthentication", ), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning", From 1157fe9ff10c57aa6b722a793d1db564a3f3a7b5 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 22 Sep 2025 17:10:45 +0200 Subject: [PATCH 2/3] Fixed tests --- modoboa/admin/models/domain.py | 2 +- modoboa/admin/models/mixins.py | 4 +++- modoboa/autoreply/api/v2/tests.py | 8 ++++---- modoboa/calendars/tests.py | 16 ++++++++-------- modoboa/contacts/tests.py | 6 +++--- modoboa/core/api/v1/tests.py | 4 ++-- modoboa/core/models.py | 2 +- 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 5821f01fa..ad5bef7dd 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -202,7 +202,7 @@ class Domain(mixins.MessageLimitMixin, AdminObject): return self.dnsrecord_set.filter(type="autodiscover").first() @cached_property - def allocated_quota(self): + def allocated_quota(self) -> int: """Return current quota allocation.""" if not self.quota: return 0 diff --git a/modoboa/admin/models/mixins.py b/modoboa/admin/models/mixins.py index 848b8b6c0..de8361f03 100644 --- a/modoboa/admin/models/mixins.py +++ b/modoboa/admin/models/mixins.py @@ -1,5 +1,7 @@ """Admin model mixins.""" +from typing import Optional + from modoboa.policyd.utils import get_message_counter @@ -18,7 +20,7 @@ class MessageLimitMixin: return self.message_limit - get_message_counter(self.message_counter_key) @property - def sent_messages_in_percent(self): + def sent_messages_in_percent(self) -> Optional[int]: # noqa """Return number of sent messages as a percentage.""" if not self.message_limit: return None diff --git a/modoboa/autoreply/api/v2/tests.py b/modoboa/autoreply/api/v2/tests.py index aee821fbb..e332d24a4 100644 --- a/modoboa/autoreply/api/v2/tests.py +++ b/modoboa/autoreply/api/v2/tests.py @@ -62,7 +62,7 @@ class ARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase): def test_retrieve_armessage_domadmin(self): admin = User.objects.get(username="admin@test.com") self.client.logout() - self.client.force_login(admin) + self.client.force_authenticate(admin) url = reverse("api:armessage-list") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -74,7 +74,7 @@ class ARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase): def test_retrieve_armessage_simpleuser(self): self.client.logout() - self.client.force_login(self.account) + self.client.force_authenticate(self.account) url = reverse("api:armessage-list") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -156,7 +156,7 @@ class AccountARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase): response = self.client.get(url) self.assertEqual(response.status_code, 403) self.client.logout() - self.client.force_login(self.account) + self.client.force_authenticate(self.account) self.assertFalse( models.ARmessage.objects.filter(mbox=self.account.mailbox).exists() ) @@ -168,7 +168,7 @@ class AccountARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase): def test_set_armessage(self): self.client.logout() - self.client.force_login(self.account) + self.client.force_authenticate(self.account) url = reverse("api:account_armessage-armessage") fromdate = timezone.now() todate = fromdate + relativedelta(days=4) diff --git a/modoboa/calendars/tests.py b/modoboa/calendars/tests.py index f92ec4e04..3f00ed214 100644 --- a/modoboa/calendars/tests.py +++ b/modoboa/calendars/tests.py @@ -13,7 +13,7 @@ from modoboa.admin import factories as admin_factories from modoboa.admin import models as admin_models from modoboa.core import factories as core_factories from modoboa.core import models as core_models -from modoboa.lib.tests import ModoTestCase, ModoAPITestCase +from modoboa.lib.tests import ModoAPITestCase from modoboa.admin.factories import populate_database @@ -53,7 +53,7 @@ class TestDataMixin: cls.scalendar2 = factories.SharedCalendarFactory(domain=cls.domain2) -class AccessRuleTestCase(ModoTestCase): +class AccessRuleTestCase(ModoAPITestCase): @classmethod def setUpTestData(cls): @@ -139,7 +139,7 @@ class UserCalendarViewSetTestCase(TestDataMixin, ModoAPITestCase): def setUp(self): """Initiate test context.""" - self.client.force_login(self.account) + self.client.force_authenticate(self.account) self.set_global_parameter("server_location", "http://localhost:5232") def test_get_calendars(self): @@ -214,7 +214,7 @@ class SharedCalendarViewSetTestCase(TestDataMixin, ModoAPITestCase): def setUp(self): """Initiate test context.""" - self.client.force_login(self.admin_account) + self.client.force_authenticate(self.admin_account) self.set_global_parameter("server_location", "http://localhost:5232") def test_get_calendars(self): @@ -304,7 +304,7 @@ class AccessRuleViewSetTestCase(TestDataMixin, ModoAPITestCase): def setUp(self): """Initiate test context.""" - self.client.force_login(self.account) + self.client.force_authenticate(self.account) def test_get_accessrules(self): """Test access rule retrieval.""" @@ -383,7 +383,7 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): self.cal_mock.return_value = mocks.Calendar(client=self.client_mock) self.addCleanup(patcher2.stop) - self.client.force_login(self.account) + self.client.force_authenticate(self.account) self.set_global_parameter("server_location", "http://localhost") def test_get_user_events(self): @@ -550,7 +550,7 @@ class AttendeeViewSetTestCase(ModoAPITestCase): def setUp(self): """Initiate test context.""" - self.client.force_login(self.account) + self.client.force_authenticate(self.account) def test_get_attendees(self): """Test attendees retrieval.""" @@ -571,7 +571,7 @@ class MailboxViewSetTestCase(ModoAPITestCase): def setUp(self): """Initiate test context.""" - self.client.force_login(self.account) + self.client.force_authenticate(self.account) def test_get_mailboxes(self): """Test mailbox retrieval.""" diff --git a/modoboa/contacts/tests.py b/modoboa/contacts/tests.py index 9225399ce..5c18b7a81 100644 --- a/modoboa/contacts/tests.py +++ b/modoboa/contacts/tests.py @@ -11,7 +11,7 @@ from django.utils import timezone from modoboa.admin import factories as admin_factories from modoboa.core import models as core_models -from modoboa.lib.tests import ModoAPITestCase, ModoTestCase +from modoboa.lib.tests import ModoAPITestCase from . import factories from . import mocks @@ -46,7 +46,7 @@ class TestDataMixin: def setUp(self, *args, **kwargs): """Initiate test context.""" - self.client.force_login(self.user) + self.client.force_authenticate(self.user) self.set_global_parameter( "server_location", "http://example.test/radicale/", app="calendars" ) @@ -312,7 +312,7 @@ class EmailAddressViewSetTestCase(TestDataMixin, ModoAPITestCase): self.assertEqual(len(response.data), 3) -class ImportTestCase(TestDataMixin, ModoTestCase): +class ImportTestCase(TestDataMixin, ModoAPITestCase): def setUp(self): super().setUp() diff --git a/modoboa/core/api/v1/tests.py b/modoboa/core/api/v1/tests.py index 0d82ed941..d4b06a798 100644 --- a/modoboa/core/api/v1/tests.py +++ b/modoboa/core/api/v1/tests.py @@ -1,9 +1,9 @@ from django.urls import reverse_lazy -from modoboa.lib.tests import ModoTestCase +from modoboa.lib.tests import ModoAPITestCase -class OpenAPITestCase(ModoTestCase): +class OpenAPITestCase(ModoAPITestCase): openapi_schema_url = reverse_lazy("schema-v1-legacy") def test_unauthorized(self): diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 129e0f0d5..e2cf99bf9 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -220,7 +220,7 @@ class User(AbstractUser): return self.email @property - def tfa_enabled(self): + def tfa_enabled(self) -> bool: return self.totp_enabled or self.webauthn_enabled def is_owner(self, obj): From 524df061c00727931101429ebe4a1208d1c4b804 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 23 Sep 2025 10:10:51 +0200 Subject: [PATCH 3/3] Fixed tests --- modoboa/contacts/tests.py | 6 ++---- modoboa/core/tests/test_ldap.py | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modoboa/contacts/tests.py b/modoboa/contacts/tests.py index 5c18b7a81..b717c37ee 100644 --- a/modoboa/contacts/tests.py +++ b/modoboa/contacts/tests.py @@ -72,8 +72,7 @@ class ViewsTestCase(TestDataMixin, ModoAPITestCase): def test_user_settings(self): """Check that remote collection creation request is sent.""" # 1. Addressbook with contacts must be synced manually - data = {"username": self.user.username, "password": "toto"} - self.client.post(reverse("core:login"), data) + self.client.force_authenticate(self.user) self.enable_cdav_sync() self.addressbook.refresh_from_db() self.assertIs(self.addressbook.last_sync, None) @@ -81,8 +80,7 @@ class ViewsTestCase(TestDataMixin, ModoAPITestCase): # 2. Addressbook with no contacts can be considered synced user = core_models.User.objects.get(username="user@test2.com") abook = user.addressbook_set.first() - data = {"username": user.username, "password": "toto"} - self.client.post(reverse("core:login"), data) + self.client.force_authenticate(user) abook.refresh_from_db() self.assertIs(abook.last_sync, None) # Now enable sync. diff --git a/modoboa/core/tests/test_ldap.py b/modoboa/core/tests/test_ldap.py index 4e386f247..0631ebf0b 100644 --- a/modoboa/core/tests/test_ldap.py +++ b/modoboa/core/tests/test_ldap.py @@ -158,6 +158,8 @@ class ProfileTestCase(LDAPTestCaseMixin, ModoAPITestCase): username = "testuser@example.com" self.authenticate(username, "test") + user = models.User.objects.get(username=username) + self.client.force_authenticate(user) response = self.client.post( reverse("v2:account-set-me-password"), {"password": "test", "new_password": "Toto1234"},