Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70d22eecfa | |||
| 7379d70321 | |||
| 29d0bbb692 | |||
| cdb766b0e0 | |||
| 38de864d68 | |||
| cad065da84 | |||
| 3893fdf4d7 | |||
| d29b5141b1 | |||
| 110fc82250 | |||
| 95f19f7c6c | |||
| ab03cd9db9 | |||
| a811431070 | |||
| d23ac76f36 | |||
| 9377a96e87 | |||
| 25313d3e84 | |||
| 9cd1b42c3d | |||
| d08198e44d | |||
| e55468862d | |||
| fbb4797f29 | |||
| 0f290df24a | |||
| a2b2c71448 | |||
| e1594493a7 | |||
| e903c5d4ca | |||
| 0f80d5f2db | |||
| 4cb695c2bf | |||
| a68f8171cb | |||
| 471f69d4ec | |||
| dc938d3159 | |||
| 87907d57de | |||
| 57fd15100e | |||
| 34c9dc6cd7 | |||
| 1be30496be | |||
| 0ca5fa5318 | |||
| 4d3901b35d | |||
| 961bceb64e | |||
| d7957547f8 | |||
| 418db6194a | |||
| c2899f66af | |||
| ba3f6a504f | |||
| 3de495a489 | |||
| e297a025c3 |
@@ -9,9 +9,9 @@ We primarily use GitHub as an issue tracker. If however you're encountering an i
|
||||
|
||||
---
|
||||
|
||||
Please make sure you have read our [main Readme](https://github.com/numerique-gouv/people).
|
||||
Please make sure you have read our [main Readme](https://github.com/suitenumerique/people).
|
||||
|
||||
Also make sure it was not already answered in [an open or close issue](https://github.com/numerique-gouv/people/issues).
|
||||
Also make sure it was not already answered in [an open or close issue](https://github.com/suitenumerique/people/issues).
|
||||
|
||||
If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌
|
||||
|
||||
|
||||
@@ -201,9 +201,6 @@ jobs:
|
||||
run: |
|
||||
make dimail-setup-db
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: cd src/frontend/apps/e2e && yarn install
|
||||
|
||||
- name: Run e2e tests
|
||||
run: cd src/frontend/ && yarn e2e:test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
|
||||
+39
-21
@@ -8,6 +8,21 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.12.0] - 2025-02-18
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(domains) allow user to re-run all fetch domain data from dimail
|
||||
- ✨(domains) display DNS config expected for domain with required actions
|
||||
- ✨(domains) check status after creation
|
||||
- ✨(domains) display required actions to do on domain
|
||||
- ✨(plugin) add CommuneCreation plugin with domain provisioning #658
|
||||
- ✨(frontend) display action required status on domain
|
||||
- ✨(domains) store last health check details on MailDomain
|
||||
- ✨(domains) add support email field on domain
|
||||
|
||||
## [1.11.0] - 2025-02-07
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(api) add count mailboxes to MailDomain serializer
|
||||
@@ -17,6 +32,7 @@ and this project adheres to
|
||||
|
||||
### Fixed
|
||||
|
||||
- ✨(auth) fix empty names from ProConnect #687
|
||||
- 🚑️(teams) do not display add button when disallowed #676
|
||||
- 🚑️(plugins) fix name from SIRET specific case #674
|
||||
- 🐛(api) restrict mailbox sync to enabled domains
|
||||
@@ -276,24 +292,26 @@ and this project adheres to
|
||||
- ✨(domains) create and manage domains on admin + API
|
||||
- ✨(domains) mailbox creation + link to email provisioning API
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/people/compare/v1.10.1...main
|
||||
[1.10.1]: https://github.com/numerique-gouv/people/releases/v1.10.1
|
||||
[1.10.0]: https://github.com/numerique-gouv/people/releases/v1.10.0
|
||||
[1.9.1]: https://github.com/numerique-gouv/people/releases/v1.9.1
|
||||
[1.9.0]: https://github.com/numerique-gouv/people/releases/v1.9.0
|
||||
[1.8.0]: https://github.com/numerique-gouv/people/releases/v1.8.0
|
||||
[1.7.1]: https://github.com/numerique-gouv/people/releases/v1.7.1
|
||||
[1.7.0]: https://github.com/numerique-gouv/people/releases/v1.7.0
|
||||
[1.6.1]: https://github.com/numerique-gouv/people/releases/v1.6.1
|
||||
[1.6.0]: https://github.com/numerique-gouv/people/releases/v1.6.0
|
||||
[1.5.0]: https://github.com/numerique-gouv/people/releases/v1.5.0
|
||||
[1.4.1]: https://github.com/numerique-gouv/people/releases/v1.4.1
|
||||
[1.4.0]: https://github.com/numerique-gouv/people/releases/v1.4.0
|
||||
[1.3.1]: https://github.com/numerique-gouv/people/releases/v1.3.1
|
||||
[1.3.0]: https://github.com/numerique-gouv/people/releases/v1.3.0
|
||||
[1.2.1]: https://github.com/numerique-gouv/people/releases/v1.2.1
|
||||
[1.2.0]: https://github.com/numerique-gouv/people/releases/v1.2.0
|
||||
[1.1.0]: https://github.com/numerique-gouv/people/releases/v1.1.0
|
||||
[1.0.2]: https://github.com/numerique-gouv/people/releases/v1.0.2
|
||||
[1.0.1]: https://github.com/numerique-gouv/people/releases/v1.0.1
|
||||
[1.0.0]: https://github.com/numerique-gouv/people/releases/v1.0.0
|
||||
[unreleased]: https://github.com/suitenumerique/people/compare/v1.12.0...main
|
||||
[1.12.0]: https://github.com/suitenumerique/people/releases/v1.12.0
|
||||
[1.11.0]: https://github.com/suitenumerique/people/releases/v1.11.0
|
||||
[1.10.1]: https://github.com/suitenumerique/people/releases/v1.10.1
|
||||
[1.10.0]: https://github.com/suitenumerique/people/releases/v1.10.0
|
||||
[1.9.1]: https://github.com/suitenumerique/people/releases/v1.9.1
|
||||
[1.9.0]: https://github.com/suitenumerique/people/releases/v1.9.0
|
||||
[1.8.0]: https://github.com/suitenumerique/people/releases/v1.8.0
|
||||
[1.7.1]: https://github.com/suitenumerique/people/releases/v1.7.1
|
||||
[1.7.0]: https://github.com/suitenumerique/people/releases/v1.7.0
|
||||
[1.6.1]: https://github.com/suitenumerique/people/releases/v1.6.1
|
||||
[1.6.0]: https://github.com/suitenumerique/people/releases/v1.6.0
|
||||
[1.5.0]: https://github.com/suitenumerique/people/releases/v1.5.0
|
||||
[1.4.1]: https://github.com/suitenumerique/people/releases/v1.4.1
|
||||
[1.4.0]: https://github.com/suitenumerique/people/releases/v1.4.0
|
||||
[1.3.1]: https://github.com/suitenumerique/people/releases/v1.3.1
|
||||
[1.3.0]: https://github.com/suitenumerique/people/releases/v1.3.0
|
||||
[1.2.1]: https://github.com/suitenumerique/people/releases/v1.2.1
|
||||
[1.2.0]: https://github.com/suitenumerique/people/releases/v1.2.0
|
||||
[1.1.0]: https://github.com/suitenumerique/people/releases/v1.1.0
|
||||
[1.0.2]: https://github.com/suitenumerique/people/releases/v1.0.2
|
||||
[1.0.1]: https://github.com/suitenumerique/people/releases/v1.0.1
|
||||
[1.0.0]: https://github.com/suitenumerique/people/releases/v1.0.0
|
||||
|
||||
@@ -41,6 +41,12 @@ RUN yarn build
|
||||
# ---- Front-end image ----
|
||||
FROM nginxinc/nginx-unprivileged:1.26-alpine AS frontend-production
|
||||
|
||||
USER root
|
||||
|
||||
RUN apk update && apk upgrade libssl3 libcrypto3
|
||||
|
||||
USER nginx
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
@@ -72,6 +72,7 @@ data/static:
|
||||
create-env-files: ## Copy the dist env files to env files
|
||||
create-env-files: \
|
||||
env.d/development/common \
|
||||
env.d/development/france \
|
||||
env.d/development/crowdin \
|
||||
env.d/development/postgresql \
|
||||
env.d/development/kc_postgresql
|
||||
@@ -228,6 +229,9 @@ resetdb: ## flush database and create a superuser "admin"
|
||||
env.d/development/common:
|
||||
cp -n env.d/development/common.dist env.d/development/common
|
||||
|
||||
env.d/development/france:
|
||||
cp -n env.d/development/france.dist env.d/development/france
|
||||
|
||||
env.d/development/postgresql:
|
||||
cp -n env.d/development/postgresql.dist env.d/development/postgresql
|
||||
|
||||
|
||||
+4
-3
@@ -9,8 +9,8 @@ services:
|
||||
redis:
|
||||
image: redis:5
|
||||
|
||||
mailcatcher:
|
||||
image: sj26/mailcatcher:latest
|
||||
maildev:
|
||||
image: maildev/maildev:latest
|
||||
ports:
|
||||
- "1081:1080"
|
||||
|
||||
@@ -27,6 +27,7 @@ services:
|
||||
- DJANGO_CONFIGURATION=Development
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/france
|
||||
- env.d/development/postgresql
|
||||
ports:
|
||||
- "8071:8000"
|
||||
@@ -37,7 +38,7 @@ services:
|
||||
depends_on:
|
||||
- dimail
|
||||
- postgresql
|
||||
- mailcatcher
|
||||
- maildev
|
||||
- redis
|
||||
|
||||
celery-dev:
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"lastName": "Delamairie",
|
||||
"enabled": true,
|
||||
"attributes": {
|
||||
"siret": "21580304000017"
|
||||
"siret": "21510339100011"
|
||||
},
|
||||
"credentials": [
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ PYTHONPATH=/app
|
||||
# People settings
|
||||
|
||||
# Mail
|
||||
DJANGO_EMAIL_HOST="mailcatcher"
|
||||
DJANGO_EMAIL_HOST="maildev"
|
||||
DJANGO_EMAIL_PORT=1025
|
||||
|
||||
# Backend url
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ORGANIZATION_PLUGINS=plugins.organizations.NameFromSiretOrganizationPlugin,plugins.organizations.CommuneCreation
|
||||
@@ -7,6 +7,7 @@ from functools import reduce
|
||||
from django.conf import settings
|
||||
from django.db.models import OuterRef, Q, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
@@ -605,7 +606,7 @@ class StatView(views.APIView):
|
||||
context = {
|
||||
"total_users": models.User.objects.count(),
|
||||
"mau": models.User.objects.filter(
|
||||
last_login__gte=datetime.datetime.now() - datetime.timedelta(30)
|
||||
last_login__gte=timezone.now() - datetime.timedelta(30)
|
||||
).count(),
|
||||
"teams": models.Team.objects.count(),
|
||||
"domains": domains_models.MailDomain.objects.count(),
|
||||
|
||||
@@ -95,13 +95,12 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
)
|
||||
|
||||
# Get user's full name from OIDC fields defined in settings
|
||||
full_name = self.compute_full_name(user_info)
|
||||
email = user_info.get("email")
|
||||
|
||||
claims = {
|
||||
"sub": sub,
|
||||
"email": email,
|
||||
"name": full_name,
|
||||
"name": self.compute_full_name(user_info),
|
||||
}
|
||||
if settings.OIDC_ORGANIZATION_REGISTRATION_ID_FIELD:
|
||||
claims[settings.OIDC_ORGANIZATION_REGISTRATION_ID_FIELD] = user_info.get(
|
||||
@@ -120,7 +119,7 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
|
||||
# Data cleaning, to be removed when user organization is null=False
|
||||
# or all users have an organization.
|
||||
# See https://github.com/numerique-gouv/people/issues/504
|
||||
# See https://github.com/suitenumerique/people/issues/504
|
||||
if not user.organization_id:
|
||||
organization_registration_id = claims.get(
|
||||
settings.OIDC_ORGANIZATION_REGISTRATION_ID_FIELD
|
||||
|
||||
@@ -240,7 +240,7 @@ class TeamWebhookFactory(factory.django.DjangoModelFactory):
|
||||
model = models.TeamWebhook
|
||||
|
||||
team = factory.SubFactory(TeamFactory)
|
||||
url = factory.Sequence(lambda n: f"https://example.com/Groups/{n!s}")
|
||||
url = factory.Sequence(lambda n: f"https://nosuchdomain.xyz/Groups/{n!s}")
|
||||
|
||||
|
||||
class InvitationFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
@@ -29,7 +29,10 @@ from timezone_field import TimeZoneField
|
||||
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
||||
|
||||
from core.enums import WebhookStatusChoices
|
||||
from core.plugins.loader import organization_plugins_run_after_create
|
||||
from core.plugins.loader import (
|
||||
organization_plugins_run_after_create,
|
||||
organization_plugins_run_after_grant_access,
|
||||
)
|
||||
from core.utils.webhooks import scim_synchronizer
|
||||
from core.validators import get_field_validators_from_setting
|
||||
|
||||
@@ -298,6 +301,22 @@ class OrganizationManager(models.Manager):
|
||||
return instance
|
||||
|
||||
|
||||
class OrganizationAccessManager(models.Manager):
|
||||
"""
|
||||
Custom manager for the OrganizationAccess model, to manage complexity/automation.
|
||||
"""
|
||||
|
||||
def create(self, **kwargs):
|
||||
"""
|
||||
Create an organization access with the given kwargs.
|
||||
|
||||
This method is overridden to call the Organization plugins.
|
||||
"""
|
||||
instance = super().create(**kwargs)
|
||||
organization_plugins_run_after_grant_access(instance)
|
||||
return instance
|
||||
|
||||
|
||||
class Organization(BaseModel):
|
||||
"""
|
||||
Organization model used to regroup Teams.
|
||||
@@ -618,6 +637,8 @@ class OrganizationAccess(BaseModel):
|
||||
default=OrganizationRoleChoices.ADMIN,
|
||||
)
|
||||
|
||||
objects = OrganizationAccessManager()
|
||||
|
||||
class Meta:
|
||||
db_table = "people_organization_access"
|
||||
verbose_name = _("Organization/user relation")
|
||||
@@ -979,3 +1000,7 @@ class Invitation(BaseModel):
|
||||
|
||||
except smtplib.SMTPException as exception:
|
||||
logger.error("invitation to %s was not sent: %s", self.email, exception)
|
||||
|
||||
|
||||
# It's not clear yet how best to split this file.
|
||||
# pylint: disable=C0302
|
||||
|
||||
@@ -11,3 +11,9 @@ class BaseOrganizationPlugin:
|
||||
def run_after_create(self, organization) -> None:
|
||||
"""Method called after creating an organization."""
|
||||
raise NotImplementedError("Plugins must implement the run_after_create method")
|
||||
|
||||
def run_after_grant_access(self, organization_access) -> None:
|
||||
"""Method called after creating an organization."""
|
||||
raise NotImplementedError(
|
||||
"Plugins must implement the run_after_grant_access method"
|
||||
)
|
||||
|
||||
@@ -30,3 +30,15 @@ def organization_plugins_run_after_create(organization):
|
||||
"""
|
||||
for plugin_instance in get_organization_plugins():
|
||||
plugin_instance.run_after_create(organization)
|
||||
|
||||
|
||||
def organization_plugins_run_after_grant_access(organization_access):
|
||||
"""
|
||||
Run the after grant access method for all organization plugins.
|
||||
|
||||
Each plugin will be called in the order they are listed in the settings.
|
||||
Each plugin is responsible to save changes if needed, this is not optimized
|
||||
but this could be easily improved later if needed.
|
||||
"""
|
||||
for plugin_instance in get_organization_plugins():
|
||||
plugin_instance.run_after_grant_access(organization_access)
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,610 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-13 07:41+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/admin.py:63
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:65
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:77
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:116
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:218
|
||||
msgid "Run post creation plugins"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:226
|
||||
msgid "Post creation plugins have been run for the selected organizations."
|
||||
msgstr ""
|
||||
|
||||
#: core/authentication/backends.py:94
|
||||
msgid "User info contained no recognizable user identification"
|
||||
msgstr ""
|
||||
|
||||
#: core/authentication/backends.py:115
|
||||
msgid "User account is disabled"
|
||||
msgstr ""
|
||||
|
||||
#: core/authentication/backends.py:161
|
||||
msgid "Claims contained no recognizable user identification"
|
||||
msgstr ""
|
||||
|
||||
#: core/authentication/backends.py:180
|
||||
msgid "Claims contained no recognizable organization identification"
|
||||
msgstr ""
|
||||
|
||||
#: core/enums.py:23
|
||||
msgid "Failure"
|
||||
msgstr ""
|
||||
|
||||
#: core/enums.py:24 mailbox_manager/enums.py:21 mailbox_manager/enums.py:31
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: core/enums.py:25
|
||||
msgid "Success"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:50
|
||||
msgid "Member"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:51 core/models.py:63 mailbox_manager/enums.py:14
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:52 mailbox_manager/enums.py:15
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:75
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:76
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:82
|
||||
msgid "created at"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:83
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:88
|
||||
msgid "updated at"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:89
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:128
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:129
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:132
|
||||
msgid "notes"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:134
|
||||
msgid "contact information"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:135
|
||||
msgid "A JSON object containing the contact information"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:149
|
||||
msgid "contact"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:150
|
||||
msgid "contacts"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:224 core/models.py:338 core/models.py:460
|
||||
#: mailbox_manager/models.py:24
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:226
|
||||
msgid "audience id"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:231
|
||||
msgid "service provider"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:232
|
||||
msgid "service providers"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:346
|
||||
msgid "registration ID list"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:353
|
||||
msgid "domain list"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:369
|
||||
msgid "organization"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:370
|
||||
msgid "organizations"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:377
|
||||
msgid "An organization must have at least a registration ID or a domain."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:445
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:451
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:453
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:459 core/models.py:901
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:465
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:466
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:472
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:475
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:477
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:480
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:482
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:485
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:488
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:507
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:508
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:644
|
||||
msgid "Organization/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:645
|
||||
msgid "Organization/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:650
|
||||
msgid "This user is already in this organization."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:727
|
||||
msgid "Team"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:728
|
||||
msgid "Teams"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:779
|
||||
msgid "Team/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:780
|
||||
msgid "Team/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:785
|
||||
msgid "This user is already in this team."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:874
|
||||
msgid "url"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:875
|
||||
msgid "secret"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:884
|
||||
msgid "Team webhook"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:885
|
||||
msgid "Team webhooks"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:918
|
||||
msgid "Team invitation"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:919
|
||||
msgid "Team invitations"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:944
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:985
|
||||
msgid "Invitation to join La Régie!"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:159 core/templates/mail/text/hello.txt:3
|
||||
msgid "Company logo"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
#, python-format
|
||||
msgid "Hello %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:189 core/templates/mail/text/hello.txt:6
|
||||
msgid "Thank you very much for your visit!"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:221
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:160
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "La Suite Numérique"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:190
|
||||
#: core/templates/mail/text/invitation.txt:5
|
||||
msgid "Invitation to join a team"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:198
|
||||
#: core/templates/mail/text/invitation.txt:8
|
||||
msgid "Welcome to"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:216
|
||||
#: core/templates/mail/text/invitation.txt:12
|
||||
msgid "Logo"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
msgid ""
|
||||
"We are delighted to welcome you to our community on La Régie, your new "
|
||||
"companion to simplify the management of your groups efficiently, "
|
||||
"intuitively, and securely."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:231
|
||||
#: core/templates/mail/text/invitation.txt:15
|
||||
msgid ""
|
||||
"Our application is designed to help you organize, collaborate, and manage "
|
||||
"permissions."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:236
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
msgid "With La Régie, you will be able to:"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:237
|
||||
#: core/templates/mail/text/invitation.txt:17
|
||||
msgid "Create customized groups according to your specific needs."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:238
|
||||
#: core/templates/mail/text/invitation.txt:18
|
||||
msgid "Invite members of your team or community in just a few clicks."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:239
|
||||
#: core/templates/mail/text/invitation.txt:19
|
||||
msgid ""
|
||||
"Plan events, meetings, or activities effortlessly with our integrated "
|
||||
"calendar."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:240
|
||||
#: core/templates/mail/text/invitation.txt:20
|
||||
msgid "Share documents, photos, and important information securely."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:241
|
||||
#: core/templates/mail/text/invitation.txt:21
|
||||
msgid ""
|
||||
"Facilitate exchanges and communication with our messaging and group "
|
||||
"discussion tools."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:252
|
||||
#: core/templates/mail/text/invitation.txt:23
|
||||
msgid "Visit La Régie"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:261
|
||||
#: core/templates/mail/text/invitation.txt:25
|
||||
msgid ""
|
||||
"We are confident that La Régie will help you increase efficiency and "
|
||||
"productivity while strengthening the bond among members."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:266
|
||||
#: core/templates/mail/text/invitation.txt:26
|
||||
msgid ""
|
||||
"Feel free to explore all the features of the application and share your "
|
||||
"feedback and suggestions with us. Your feedback is valuable to us and will "
|
||||
"enable us to continually improve our service."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:271
|
||||
#: core/templates/mail/text/invitation.txt:27
|
||||
msgid ""
|
||||
"Once again, welcome aboard! We are eager to accompany you on this group "
|
||||
"management adventure."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:278
|
||||
#: core/templates/mail/html/new_mailbox.html:272
|
||||
#: core/templates/mail/text/invitation.txt:29
|
||||
#: core/templates/mail/text/new_mailbox.txt:15
|
||||
msgid "Sincerely,"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:279
|
||||
#: core/templates/mail/text/invitation.txt:31
|
||||
msgid "The La Suite Numérique Team"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/new_mailbox.html:159
|
||||
#: core/templates/mail/text/new_mailbox.txt:3
|
||||
msgid "La Messagerie"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/new_mailbox.html:188
|
||||
#: core/templates/mail/text/new_mailbox.txt:5
|
||||
msgid "Welcome to La Messagerie"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/new_mailbox.html:193
|
||||
#: core/templates/mail/text/new_mailbox.txt:6
|
||||
msgid "La Messagerie is the email solution of La Suite."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/new_mailbox.html:199
|
||||
#: core/templates/mail/text/new_mailbox.txt:7
|
||||
msgid "Your mailbox has been created."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/new_mailbox.html:204
|
||||
#: core/templates/mail/text/new_mailbox.txt:8
|
||||
msgid "Please find below your login info: "
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/new_mailbox.html:228
|
||||
#: core/templates/mail/text/new_mailbox.txt:10
|
||||
msgid "Email address: "
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/new_mailbox.html:233
|
||||
#: core/templates/mail/text/new_mailbox.txt:11
|
||||
msgid "Temporary password (to be modify on first login): "
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/new_mailbox.html:261
|
||||
#: core/templates/mail/text/new_mailbox.txt:13
|
||||
msgid "Go to La Messagerie"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/new_mailbox.html:273
|
||||
#: core/templates/mail/text/new_mailbox.txt:17
|
||||
msgid "La Suite Team"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/text/hello.txt:8
|
||||
#, python-format
|
||||
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:16
|
||||
msgid "Synchronise from dimail"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:34
|
||||
#, python-brace-format
|
||||
msgid "Synchronisation failed for {domain.name} with message: [{err}]"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:40
|
||||
#, python-brace-format
|
||||
msgid "Synchronisation succeed for {domain.name}. "
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:48
|
||||
msgid ""
|
||||
"Sync require enabled domains. Excluded domains: {', '.join(excluded_domains)}"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:53
|
||||
msgid "Check and update status from dimail"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:69
|
||||
#, python-brace-format
|
||||
msgid "- {domain.name} with message: '{err}'"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:80
|
||||
msgid "Check domains done with success."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:81
|
||||
msgid "Domains updated: {', '.join(domains_updated)}"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:83
|
||||
msgid "No domain updated."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:90
|
||||
msgid "Check domain failed for:"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:99
|
||||
msgid "Domains disabled are excluded from check: {', '.join(excluded_domains)}"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:13
|
||||
msgid "Viewer"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:22 mailbox_manager/enums.py:32
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:23 mailbox_manager/enums.py:33
|
||||
msgid "Failed"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:24 mailbox_manager/enums.py:34
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:25
|
||||
msgid "Action required"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:32
|
||||
msgid "support email"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:36
|
||||
msgid "last check details"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:37
|
||||
msgid "A JSON object containing the last health check details"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:42
|
||||
msgid "Mail domain"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:43
|
||||
msgid "Mail domains"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:110
|
||||
msgid "User/mail domain relation"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:111
|
||||
msgid "User/mail domain relations"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:184
|
||||
msgid "local_part"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:198
|
||||
msgid "secondary email address"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:208
|
||||
msgid "Mailbox"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:209
|
||||
msgid "Mailboxes"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:234
|
||||
msgid "You can't create or update a mailbox for a disabled domain."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/utils/dimail.py:266
|
||||
msgid "Your new mailbox information"
|
||||
msgstr ""
|
||||
|
||||
#: people/settings.py:146
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: people/settings.py:147
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
Binary file not shown.
@@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-people\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-03 10:27+0000\n"
|
||||
"POT-Creation-Date: 2025-02-13 08:30+0000\n"
|
||||
"PO-Revision-Date: 2024-01-03 23:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
@@ -49,17 +49,17 @@ msgstr ""
|
||||
"Les informations de l'utilisateur ne contiennent aucune identification "
|
||||
"reconnaissable"
|
||||
|
||||
#: core/authentication/backends.py:116
|
||||
#: core/authentication/backends.py:115
|
||||
msgid "User account is disabled"
|
||||
msgstr "Le compte de l'utilisateur est désactivé"
|
||||
|
||||
#: core/authentication/backends.py:162
|
||||
#: core/authentication/backends.py:161
|
||||
msgid "Claims contained no recognizable user identification"
|
||||
msgstr ""
|
||||
"Les claims ne contiennent aucune identification reconnaissable pour "
|
||||
"l'utilisateur"
|
||||
|
||||
#: core/authentication/backends.py:181
|
||||
#: core/authentication/backends.py:180
|
||||
msgid "Claims contained no recognizable organization identification"
|
||||
msgstr ""
|
||||
"Les claims ne contiennent aucune identification reconnaissable pour "
|
||||
@@ -77,110 +77,110 @@ msgstr "En attente"
|
||||
msgid "Success"
|
||||
msgstr "Réussi"
|
||||
|
||||
#: core/models.py:47
|
||||
#: core/models.py:50
|
||||
msgid "Member"
|
||||
msgstr "Membre"
|
||||
|
||||
#: core/models.py:48 core/models.py:60 mailbox_manager/enums.py:14
|
||||
#: core/models.py:51 core/models.py:63 mailbox_manager/enums.py:14
|
||||
msgid "Administrator"
|
||||
msgstr "Administrateur"
|
||||
|
||||
#: core/models.py:49 mailbox_manager/enums.py:15
|
||||
#: core/models.py:52 mailbox_manager/enums.py:15
|
||||
msgid "Owner"
|
||||
msgstr "Propriétaire"
|
||||
|
||||
#: core/models.py:72
|
||||
#: core/models.py:75
|
||||
msgid "id"
|
||||
msgstr "identifiant"
|
||||
|
||||
#: core/models.py:73
|
||||
#: core/models.py:76
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "Clé primaire pour l'enregistrement en tant que UUID"
|
||||
|
||||
#: core/models.py:79
|
||||
#: core/models.py:82
|
||||
msgid "created at"
|
||||
msgstr "Créé le"
|
||||
|
||||
#: core/models.py:80
|
||||
#: core/models.py:83
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "Date et heure de création de l'enregistrement"
|
||||
|
||||
#: core/models.py:85
|
||||
#: core/models.py:88
|
||||
msgid "updated at"
|
||||
msgstr "mis à jour le"
|
||||
|
||||
#: core/models.py:86
|
||||
#: core/models.py:89
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "date et heure de la dernière mise à jour de l'enregistrement"
|
||||
|
||||
#: core/models.py:125
|
||||
#: core/models.py:128
|
||||
msgid "full name"
|
||||
msgstr "nom complet"
|
||||
|
||||
#: core/models.py:126
|
||||
#: core/models.py:129
|
||||
msgid "short name"
|
||||
msgstr "nom court"
|
||||
|
||||
#: core/models.py:129
|
||||
#: core/models.py:132
|
||||
msgid "notes"
|
||||
msgstr "notes"
|
||||
|
||||
#: core/models.py:131
|
||||
#: core/models.py:134
|
||||
msgid "contact information"
|
||||
msgstr "informations de contact"
|
||||
|
||||
#: core/models.py:132
|
||||
#: core/models.py:135
|
||||
msgid "A JSON object containing the contact information"
|
||||
msgstr "Un objet JSON contenant les informations de contact"
|
||||
|
||||
#: core/models.py:146
|
||||
#: core/models.py:149
|
||||
msgid "contact"
|
||||
msgstr "contact"
|
||||
|
||||
#: core/models.py:147
|
||||
#: core/models.py:150
|
||||
msgid "contacts"
|
||||
msgstr "contacts"
|
||||
|
||||
#: core/models.py:221 core/models.py:319 core/models.py:441
|
||||
#: core/models.py:224 core/models.py:338 core/models.py:460
|
||||
#: mailbox_manager/models.py:24
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: core/models.py:223
|
||||
#: core/models.py:226
|
||||
msgid "audience id"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:228
|
||||
#: core/models.py:231
|
||||
msgid "service provider"
|
||||
msgstr "fournisseur de service"
|
||||
|
||||
#: core/models.py:229
|
||||
#: core/models.py:232
|
||||
msgid "service providers"
|
||||
msgstr "fournisseurs de service"
|
||||
|
||||
#: core/models.py:327
|
||||
#: core/models.py:346
|
||||
msgid "registration ID list"
|
||||
msgstr "liste d'identifiants d'inscription"
|
||||
|
||||
#: core/models.py:334
|
||||
#: core/models.py:353
|
||||
msgid "domain list"
|
||||
msgstr "liste de domaines"
|
||||
|
||||
#: core/models.py:350
|
||||
#: core/models.py:369
|
||||
msgid "organization"
|
||||
msgstr "organisation"
|
||||
|
||||
#: core/models.py:351
|
||||
#: core/models.py:370
|
||||
msgid "organizations"
|
||||
msgstr "organisations"
|
||||
|
||||
#: core/models.py:358
|
||||
#: core/models.py:377
|
||||
msgid "An organization must have at least a registration ID or a domain."
|
||||
msgstr ""
|
||||
"Une organisation doit avoir au moins un identifiant d'inscription ou un "
|
||||
"domaine."
|
||||
|
||||
#: core/models.py:426
|
||||
#: core/models.py:445
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -188,11 +188,11 @@ msgstr ""
|
||||
"Entrez une sub valide. Cette valeur peut contenir uniquement des lettres, "
|
||||
"des chiffres et des caractères @/./+/-/_"
|
||||
|
||||
#: core/models.py:432
|
||||
#: core/models.py:451
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:434
|
||||
#: core/models.py:453
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
@@ -200,43 +200,43 @@ msgstr ""
|
||||
"Requis. 255 caractères ou moins. Lettres, chiffres et caractères @/./+/-/_ "
|
||||
"uniquement."
|
||||
|
||||
#: core/models.py:440 core/models.py:880
|
||||
#: core/models.py:459 core/models.py:901
|
||||
msgid "email address"
|
||||
msgstr "adresse email"
|
||||
|
||||
#: core/models.py:446
|
||||
#: core/models.py:465
|
||||
msgid "language"
|
||||
msgstr "langue"
|
||||
|
||||
#: core/models.py:447
|
||||
#: core/models.py:466
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface."
|
||||
|
||||
#: core/models.py:453
|
||||
#: core/models.py:472
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||
|
||||
#: core/models.py:456
|
||||
#: core/models.py:475
|
||||
msgid "device"
|
||||
msgstr "appareil"
|
||||
|
||||
#: core/models.py:458
|
||||
#: core/models.py:477
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||
|
||||
#: core/models.py:461
|
||||
#: core/models.py:480
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:463
|
||||
#: core/models.py:482
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si l'utilisateur peut se connecter à cette interface d'administration."
|
||||
|
||||
#: core/models.py:466
|
||||
#: core/models.py:485
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:469
|
||||
#: core/models.py:488
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -244,75 +244,75 @@ msgstr ""
|
||||
"Si cet utilisateur doit être considéré comme actif. Désélectionnez cette "
|
||||
"option au lieu de supprimer les comptes."
|
||||
|
||||
#: core/models.py:488
|
||||
#: core/models.py:507
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: core/models.py:489
|
||||
#: core/models.py:508
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: core/models.py:623
|
||||
#: core/models.py:644
|
||||
msgid "Organization/user relation"
|
||||
msgstr "Relation entre une organisation et un utilisateur"
|
||||
|
||||
#: core/models.py:624
|
||||
#: core/models.py:645
|
||||
msgid "Organization/user relations"
|
||||
msgstr "Relations entre une organisation et un utilisateur"
|
||||
|
||||
#: core/models.py:629
|
||||
#: core/models.py:650
|
||||
msgid "This user is already in this organization."
|
||||
msgstr "Cet utilisateur est déjà dans cette organisation."
|
||||
|
||||
#: core/models.py:706
|
||||
#: core/models.py:727
|
||||
msgid "Team"
|
||||
msgstr "Equipe"
|
||||
|
||||
#: core/models.py:707
|
||||
#: core/models.py:728
|
||||
msgid "Teams"
|
||||
msgstr "Equipes"
|
||||
|
||||
#: core/models.py:758
|
||||
#: core/models.py:779
|
||||
msgid "Team/user relation"
|
||||
msgstr "Relation entre une équipe et un utilisateur"
|
||||
|
||||
#: core/models.py:759
|
||||
#: core/models.py:780
|
||||
msgid "Team/user relations"
|
||||
msgstr "Relations entre une équipe et un utilisateur"
|
||||
|
||||
#: core/models.py:764
|
||||
#: core/models.py:785
|
||||
msgid "This user is already in this team."
|
||||
msgstr "Cet utilisateur est déjà dans cette équipe."
|
||||
|
||||
#: core/models.py:853
|
||||
#: core/models.py:874
|
||||
msgid "url"
|
||||
msgstr "url"
|
||||
|
||||
#: core/models.py:854
|
||||
#: core/models.py:875
|
||||
msgid "secret"
|
||||
msgstr "secret"
|
||||
|
||||
#: core/models.py:863
|
||||
#: core/models.py:884
|
||||
msgid "Team webhook"
|
||||
msgstr "Webhook d'équipe"
|
||||
|
||||
#: core/models.py:864
|
||||
#: core/models.py:885
|
||||
msgid "Team webhooks"
|
||||
msgstr "Webhooks d'équipe"
|
||||
|
||||
#: core/models.py:897
|
||||
#: core/models.py:918
|
||||
msgid "Team invitation"
|
||||
msgstr "Invitation d'équipe"
|
||||
|
||||
#: core/models.py:898
|
||||
#: core/models.py:919
|
||||
msgid "Team invitations"
|
||||
msgstr "Invitations d'équipe"
|
||||
|
||||
#: core/models.py:923
|
||||
#: core/models.py:944
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Cette adresse email est déjà associée à un utilisateur enregistré."
|
||||
|
||||
#: core/models.py:965 core/models.py:971
|
||||
#: core/models.py:985
|
||||
msgid "Invitation to join La Régie!"
|
||||
msgstr "Invitation à rejoindre La Régie!"
|
||||
|
||||
@@ -519,46 +519,53 @@ msgstr "L'équipe de La Suite"
|
||||
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||
msgstr "Cette mail a été envoyée à %(email)s par %(name)s [%(href)s]"
|
||||
|
||||
#: mailbox_manager/admin.py:13
|
||||
#: mailbox_manager/admin.py:16
|
||||
msgid "Synchronise from dimail"
|
||||
msgstr "Synchroniser à partir de dimail"
|
||||
|
||||
#: mailbox_manager/admin.py:24
|
||||
#: mailbox_manager/admin.py:34
|
||||
#, python-brace-format
|
||||
msgid "Synchronisation failed for {domain.name} with message: [{err}]"
|
||||
msgstr "Synchronisation échouée pour {domain.name} avec le message: [{err}]"
|
||||
|
||||
#: mailbox_manager/admin.py:30
|
||||
#: mailbox_manager/admin.py:40
|
||||
#, python-brace-format
|
||||
msgid "Synchronisation succeed for {domain.name}. "
|
||||
msgstr "Synchronisation réussie pour {domain.name}. "
|
||||
|
||||
#: mailbox_manager/admin.py:36
|
||||
#: mailbox_manager/admin.py:48
|
||||
msgid ""
|
||||
"Sync require enabled domains. Excluded domains: {', '.join(excluded_domains)}"
|
||||
msgstr ""
|
||||
"Le synchro nécessite des domaines activés. Les domaines exclus sont : {', '."
|
||||
"join(excluded_domains)}"
|
||||
|
||||
#: mailbox_manager/admin.py:53
|
||||
msgid "Check and update status from dimail"
|
||||
msgstr "Vérification et mise à jour de l'état à partir de dimail"
|
||||
|
||||
#: mailbox_manager/admin.py:52
|
||||
#: mailbox_manager/admin.py:69
|
||||
#, python-brace-format
|
||||
msgid "- <b>{domain.name}</b> with message: '{err}'"
|
||||
msgstr "- <b>{domain.name}</b> avec le message: '{err}'"
|
||||
msgid "- {domain.name} with message: '{err}'"
|
||||
msgstr "- {domain.name} avec le message: '{err}'"
|
||||
|
||||
#: mailbox_manager/admin.py:63
|
||||
#: mailbox_manager/admin.py:80
|
||||
msgid "Check domains done with success."
|
||||
msgstr "Vérification des domains réalisée avec success."
|
||||
|
||||
#: mailbox_manager/admin.py:64
|
||||
#: mailbox_manager/admin.py:81
|
||||
msgid "Domains updated: {', '.join(domains_updated)}"
|
||||
msgstr "Domaines mis à jour : {', '.join(domains_updated)}"
|
||||
|
||||
#: mailbox_manager/admin.py:66
|
||||
#: mailbox_manager/admin.py:83
|
||||
msgid "No domain updated."
|
||||
msgstr "Aucun domain n'a été mis à jour."
|
||||
|
||||
#: mailbox_manager/admin.py:70
|
||||
#: mailbox_manager/admin.py:90
|
||||
msgid "Check domain failed for:"
|
||||
msgstr "La vérification de domain a échoué pour :"
|
||||
|
||||
#: mailbox_manager/admin.py:76
|
||||
#: mailbox_manager/admin.py:99
|
||||
msgid "Domains disabled are excluded from check: {', '.join(excluded_domains)}"
|
||||
msgstr ""
|
||||
"Les domains désactivés sont exclus de la vérification : {', '."
|
||||
@@ -584,39 +591,51 @@ msgstr "Désactivé"
|
||||
msgid "Action required"
|
||||
msgstr "Action requise"
|
||||
|
||||
#: mailbox_manager/models.py:35
|
||||
#: mailbox_manager/models.py:32
|
||||
msgid "support email"
|
||||
msgstr "adresse email du support"
|
||||
|
||||
#: mailbox_manager/models.py:36
|
||||
msgid "last check details"
|
||||
msgstr "détails de la dernière vérification"
|
||||
|
||||
#: mailbox_manager/models.py:37
|
||||
msgid "A JSON object containing the last health check details"
|
||||
msgstr "Un objet JSON contenant les détails de la dernière vérification"
|
||||
|
||||
#: mailbox_manager/models.py:42
|
||||
msgid "Mail domain"
|
||||
msgstr "Domaine de messagerie"
|
||||
|
||||
#: mailbox_manager/models.py:36
|
||||
#: mailbox_manager/models.py:43
|
||||
msgid "Mail domains"
|
||||
msgstr "Domaines de messagerie"
|
||||
|
||||
#: mailbox_manager/models.py:102
|
||||
#: mailbox_manager/models.py:110
|
||||
msgid "User/mail domain relation"
|
||||
msgstr "Relation entre un utilisateur et un domaine de mail"
|
||||
|
||||
#: mailbox_manager/models.py:103
|
||||
#: mailbox_manager/models.py:111
|
||||
msgid "User/mail domain relations"
|
||||
msgstr "Relations entre un utilisateur et un domaine de mail"
|
||||
|
||||
#: mailbox_manager/models.py:175
|
||||
#: mailbox_manager/models.py:184
|
||||
msgid "local_part"
|
||||
msgstr "local_part"
|
||||
|
||||
#: mailbox_manager/models.py:189
|
||||
#: mailbox_manager/models.py:198
|
||||
msgid "secondary email address"
|
||||
msgstr "adresse email secondaire"
|
||||
|
||||
#: mailbox_manager/models.py:199
|
||||
#: mailbox_manager/models.py:208
|
||||
msgid "Mailbox"
|
||||
msgstr "Boîte mail"
|
||||
|
||||
#: mailbox_manager/models.py:200
|
||||
#: mailbox_manager/models.py:209
|
||||
msgid "Mailboxes"
|
||||
msgstr "Boîtes mails"
|
||||
|
||||
#: mailbox_manager/models.py:224
|
||||
#: mailbox_manager/models.py:234
|
||||
msgid "You can't create or update a mailbox for a disabled domain."
|
||||
msgstr ""
|
||||
"Vous ne pouvez pas créer ou modifier une boîte mail pour un domain désactivé."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Admin classes and registrations for People's mailbox manager app."""
|
||||
|
||||
from django.contrib import admin, messages
|
||||
from django.utils.html import format_html
|
||||
from django.utils.html import format_html_join, mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from requests import exceptions
|
||||
@@ -9,6 +9,9 @@ from requests import exceptions
|
||||
from mailbox_manager import enums, models
|
||||
from mailbox_manager.utils.dimail import DimailAPIClient
|
||||
|
||||
# Prevent Ruff complaining about mark_safe below
|
||||
# ruff: noqa: S308
|
||||
|
||||
|
||||
@admin.action(description=_("Synchronise from dimail"))
|
||||
def sync_mailboxes_from_dimail(modeladmin, request, queryset): # pylint: disable=unused-argument
|
||||
@@ -63,7 +66,7 @@ def fetch_domain_status_from_dimail(modeladmin, request, queryset): # pylint: d
|
||||
try:
|
||||
response = client.fetch_domain_status(domain)
|
||||
except exceptions.HTTPError as err:
|
||||
msg_error.append(_(f"""- <b>{domain.name}</b> with message: '{err}'"""))
|
||||
msg_error.append(_(f"- {domain.name} with message: '{err}'"))
|
||||
else:
|
||||
success = True
|
||||
# temporary (or not?) display content of the dimail response to debug broken state
|
||||
@@ -79,10 +82,16 @@ def fetch_domain_status_from_dimail(modeladmin, request, queryset): # pylint: d
|
||||
if domains_updated
|
||||
else _("No domain updated."),
|
||||
]
|
||||
messages.success(request, format_html("<br> ".join(map(str, msg_success))))
|
||||
messages.success(
|
||||
request,
|
||||
format_html_join(mark_safe("<br> "), "{}", ([str(m)] for m in msg_success)),
|
||||
)
|
||||
if msg_error:
|
||||
msg_error.insert(0, _("Check domain failed for:"))
|
||||
messages.error(request, format_html("<br> ".join(map(str, msg_error))))
|
||||
messages.error(
|
||||
request,
|
||||
format_html_join(mark_safe("<br> "), "{}", ([str(m)] for m in msg_error)),
|
||||
)
|
||||
if excluded_domains:
|
||||
messages.warning(
|
||||
request,
|
||||
@@ -92,6 +101,35 @@ def fetch_domain_status_from_dimail(modeladmin, request, queryset): # pylint: d
|
||||
)
|
||||
|
||||
|
||||
@admin.action(description=_("Fetch domain expected config from dimail"))
|
||||
def fetch_domain_expected_config_from_dimail(modeladmin, request, queryset): # pylint: disable=unused-argument
|
||||
"""Admin action to fetch domain expected config from dimail."""
|
||||
client = DimailAPIClient()
|
||||
excluded_domains = []
|
||||
for domain in queryset:
|
||||
# do not check disabled domains
|
||||
if domain.status == enums.MailDomainStatusChoices.DISABLED:
|
||||
excluded_domains.append(domain.name)
|
||||
continue
|
||||
response = client.fetch_domain_expected_config(domain)
|
||||
if response:
|
||||
messages.success(
|
||||
request,
|
||||
_(f"Domain expected config fetched with success for {domain.name}."),
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
request, _(f"Failed to fetch domain expected config for {domain.name}.")
|
||||
)
|
||||
if excluded_domains:
|
||||
messages.warning(
|
||||
request,
|
||||
_(
|
||||
f"Domains disabled are excluded from fetch: {', '.join(excluded_domains)}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class UserMailDomainAccessInline(admin.TabularInline):
|
||||
"""Inline admin class for mail domain accesses."""
|
||||
|
||||
@@ -115,7 +153,11 @@ class MailDomainAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ["created_at", "slug"]
|
||||
list_filter = ("status",)
|
||||
inlines = (UserMailDomainAccessInline,)
|
||||
actions = (sync_mailboxes_from_dimail, fetch_domain_status_from_dimail)
|
||||
actions = (
|
||||
sync_mailboxes_from_dimail,
|
||||
fetch_domain_status_from_dimail,
|
||||
fetch_domain_expected_config_from_dimail,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(models.Mailbox)
|
||||
|
||||
@@ -57,6 +57,7 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
|
||||
abilities = serializers.SerializerMethodField(read_only=True)
|
||||
count_mailboxes = serializers.SerializerMethodField(read_only=True)
|
||||
action_required_details = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.MailDomain
|
||||
@@ -70,6 +71,10 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"count_mailboxes",
|
||||
"support_email",
|
||||
"last_check_details",
|
||||
"action_required_details",
|
||||
"expected_config",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
@@ -79,8 +84,24 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"count_mailboxes",
|
||||
"last_check_details",
|
||||
"action_required_details",
|
||||
"expected_config",
|
||||
]
|
||||
|
||||
def get_action_required_details(self, domain) -> dict:
|
||||
"""Return last check details of the domain."""
|
||||
details = {}
|
||||
if domain.last_check_details:
|
||||
for check, value in domain.last_check_details.items():
|
||||
if (
|
||||
isinstance(value, dict)
|
||||
and value.get("ok") is False
|
||||
and value.get("internal") is False
|
||||
):
|
||||
details[check] = value["errors"][0].get("detail")
|
||||
return details
|
||||
|
||||
def get_abilities(self, domain) -> dict:
|
||||
"""Return abilities of the logged-in user on the instance."""
|
||||
request = self.context.get("request")
|
||||
@@ -99,9 +120,18 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
# send new domain request to dimail
|
||||
client = DimailAPIClient()
|
||||
client.create_domain(validated_data["name"], self.context["request"].user.sub)
|
||||
|
||||
# no exception raised ? Then actually save domain on our database
|
||||
return super().create(validated_data)
|
||||
domain = super().create(validated_data)
|
||||
# check domain status and update it
|
||||
try:
|
||||
client.fetch_domain_status(domain)
|
||||
client.fetch_domain_expected_config(domain)
|
||||
except HTTPError as e:
|
||||
logger.exception(
|
||||
"[DIMAIL] domain status fetch after creation failed %s with error %s",
|
||||
domain.name,
|
||||
e,
|
||||
)
|
||||
return domain
|
||||
|
||||
|
||||
class MailDomainAccessSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -33,7 +33,11 @@ class MailDomainViewSet(
|
||||
|
||||
POST /api/<version>/mail-domains/ with expected data:
|
||||
- name: str
|
||||
- support_email: str
|
||||
Return newly created domain
|
||||
|
||||
POST /api/<version>/mail-domains/<domain-slug>/fetch/
|
||||
Fetch domain status and expected config from dimail.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AccessPermission]
|
||||
@@ -60,6 +64,17 @@ class MailDomainViewSet(
|
||||
}
|
||||
)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="fetch")
|
||||
def fetch_from_dimail(self, request, *args, **kwargs):
|
||||
"""Fetch domain status and expected config from dimail."""
|
||||
domain = self.get_object()
|
||||
client = DimailAPIClient()
|
||||
client.fetch_domain_status(domain)
|
||||
client.fetch_domain_expected_config(domain)
|
||||
return Response(
|
||||
serializers.MailDomainSerializer(domain, context={"request": request}).data
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
class MailDomainAccessViewSet(
|
||||
|
||||
@@ -27,6 +27,7 @@ class MailDomainFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
name = factory.Faker("domain_name")
|
||||
slug = factory.LazyAttribute(lambda o: slugify(o.name))
|
||||
support_email = factory.Faker("email")
|
||||
|
||||
@factory.post_generation
|
||||
def users(self, create, extracted, **kwargs):
|
||||
|
||||
@@ -48,7 +48,7 @@ class Command(BaseCommand):
|
||||
# protected behind admin rights but dimail allows to create a first user
|
||||
# when database is empty
|
||||
self.create_user(
|
||||
auth=(None, None),
|
||||
auth=("", ""),
|
||||
name=admin["username"],
|
||||
password=admin["password"],
|
||||
perms=[],
|
||||
@@ -66,7 +66,11 @@ class Command(BaseCommand):
|
||||
# we create a domain and add John Doe to it
|
||||
domain_name = "test.domain.com"
|
||||
domain = MailDomain.objects.get_or_create(
|
||||
name=domain_name, defaults={"status": enums.MailDomainStatusChoices.ENABLED}
|
||||
name=domain_name,
|
||||
defaults={
|
||||
"status": enums.MailDomainStatusChoices.ENABLED,
|
||||
"support_email": f"support@{domain_name}",
|
||||
},
|
||||
)[0]
|
||||
self.create_domain(domain_name)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-02 21:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def fill_support_email(apps, schema_editor):
|
||||
MailDomain = apps.get_model('mailbox_manager', 'MailDomain')
|
||||
for domain in MailDomain.objects.filter(support_email__isnull=True):
|
||||
domain.support_email = "support-regie@numerique.gouv.fr"
|
||||
domain.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0017_alter_maildomain_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='maildomain',
|
||||
name='support_email',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='support email'),
|
||||
),
|
||||
migrations.RunPython(fill_support_email, reverse_code=migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name='maildomain',
|
||||
name='support_email',
|
||||
field=models.EmailField(max_length=254, verbose_name='support email'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-07 20:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0018_maildomain_support_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='maildomain',
|
||||
name='last_check_details',
|
||||
field=models.JSONField(blank=True, help_text='A JSON object containing the last health check details', null=True, verbose_name='last check details'),
|
||||
),
|
||||
]
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.6 on 2025-02-14 17:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0019_maildomain_last_check_details'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='mailbox',
|
||||
options={'ordering': ['-created_at'], 'verbose_name': 'Mailbox', 'verbose_name_plural': 'Mailboxes'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='maildomain',
|
||||
options={'ordering': ['-created_at'], 'verbose_name': 'Mail domain', 'verbose_name_plural': 'Mail domains'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='maildomainaccess',
|
||||
options={'ordering': ['-created_at'], 'verbose_name': 'User/mail domain relation', 'verbose_name_plural': 'User/mail domain relations'},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.6 on 2025-02-14 17:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0020_alter_mailbox_options_alter_maildomain_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='maildomain',
|
||||
name='expected_config',
|
||||
field=models.JSONField(blank=True, help_text='A JSON object containing the expected config', null=True, verbose_name='expected config'),
|
||||
),
|
||||
]
|
||||
@@ -29,11 +29,25 @@ class MailDomain(BaseModel):
|
||||
default=MailDomainStatusChoices.PENDING,
|
||||
choices=MailDomainStatusChoices.choices,
|
||||
)
|
||||
support_email = models.EmailField(_("support email"), null=False, blank=False)
|
||||
last_check_details = models.JSONField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("last check details"),
|
||||
help_text=_("A JSON object containing the last health check details"),
|
||||
)
|
||||
expected_config = models.JSONField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("expected config"),
|
||||
help_text=_("A JSON object containing the expected config"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "people_mail_domain"
|
||||
verbose_name = _("Mail domain")
|
||||
verbose_name_plural = _("Mail domains")
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -102,6 +116,7 @@ class MailDomainAccess(BaseModel):
|
||||
verbose_name = _("User/mail domain relation")
|
||||
verbose_name_plural = _("User/mail domain relations")
|
||||
unique_together = ("user", "domain")
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"Access of user {self.user} on domain {self.domain}."
|
||||
@@ -199,6 +214,7 @@ class Mailbox(BaseModel):
|
||||
verbose_name = _("Mailbox")
|
||||
verbose_name_plural = _("Mailboxes")
|
||||
unique_together = ("local_part", "domain")
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.local_part!s}@{self.domain.name:s}"
|
||||
|
||||
+242
-138
@@ -2,6 +2,7 @@
|
||||
Tests for MailDomains API endpoint in People's app mailbox_manager. Focus on "create" action.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -14,6 +15,11 @@ from rest_framework.test import APIClient
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories, models
|
||||
from mailbox_manager.tests.fixtures.dimail import (
|
||||
CHECK_DOMAIN_BROKEN,
|
||||
CHECK_DOMAIN_OK,
|
||||
DOMAIN_SPEC,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -52,6 +58,7 @@ def test_api_mail_domains__create_name_unique():
|
||||
assert response.json()["name"] == ["Mail domain with this name already exists."]
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__create_authenticated():
|
||||
"""
|
||||
Authenticated users should be able to create mail domains
|
||||
@@ -64,44 +71,64 @@ def test_api_mail_domains__create_authenticated():
|
||||
|
||||
domain_name = "test.domain.fr"
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/domains/"),
|
||||
body=str(
|
||||
{
|
||||
"name": domain_name,
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/users/"),
|
||||
body=str(
|
||||
{
|
||||
"name": "request-user-sub",
|
||||
"is_admin": "false",
|
||||
"uuid": "user-uuid-on-dimail",
|
||||
"perms": [],
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/allows/"),
|
||||
body=str({"user": "request-user-sub", "domain": str(domain_name)}),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.post(
|
||||
"/api/v1.0/mail-domains/",
|
||||
{"name": domain_name, "context": "null", "features": ["webmail"]},
|
||||
format="json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/domains/"),
|
||||
body=str(
|
||||
{
|
||||
"name": domain_name,
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/users/"),
|
||||
body=str(
|
||||
{
|
||||
"name": "request-user-sub",
|
||||
"is_admin": "false",
|
||||
"uuid": "user-uuid-on-dimail",
|
||||
"perms": [],
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/allows/"),
|
||||
body=str({"user": "request-user-sub", "domain": str(domain_name)}),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
body_content_domain1 = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_content_domain1["name"] = domain_name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain_name}/check/"),
|
||||
body=json.dumps(body_content_domain1),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain_name}/spec/"),
|
||||
body=json.dumps(DOMAIN_SPEC),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.post(
|
||||
"/api/v1.0/mail-domains/",
|
||||
{
|
||||
"name": domain_name,
|
||||
"context": "null",
|
||||
"features": ["webmail"],
|
||||
"support_email": f"support@{domain_name}",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
domain = models.MailDomain.objects.get()
|
||||
|
||||
@@ -110,25 +137,43 @@ def test_api_mail_domains__create_authenticated():
|
||||
"id": str(domain.id),
|
||||
"name": domain.name,
|
||||
"slug": domain.slug,
|
||||
"status": enums.MailDomainStatusChoices.PENDING,
|
||||
"status": enums.MailDomainStatusChoices.ACTION_REQUIRED,
|
||||
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
"count_mailboxes": 0,
|
||||
"support_email": domain.support_email,
|
||||
"last_check_details": body_content_domain1,
|
||||
"action_required_details": {
|
||||
"cname_imap": "Il faut un CNAME 'imap.example.fr' qui renvoie vers "
|
||||
"'imap.ox.numerique.gouv.fr.'",
|
||||
"cname_smtp": "Le CNAME pour 'smtp.example.fr' n'est pas bon, "
|
||||
"il renvoie vers 'ns0.ovh.net.' et je veux 'smtp.ox.numerique.gouv.fr.'",
|
||||
"cname_webmail": "Il faut un CNAME 'webmail.example.fr' qui "
|
||||
"renvoie vers 'webmail.ox.numerique.gouv.fr.'",
|
||||
"dkim": "Il faut un DKIM record, avec la bonne clef",
|
||||
"mx": "Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr., "
|
||||
"or je trouve example-fr.mail.protection.outlook.com.",
|
||||
"spf": "Le SPF record ne contient pas include:_spf.ox.numerique.gouv.fr",
|
||||
},
|
||||
"expected_config": DOMAIN_SPEC,
|
||||
}
|
||||
|
||||
# a new domain with status "pending" is created and authenticated user is the owner
|
||||
assert domain.status == enums.MailDomainStatusChoices.PENDING
|
||||
# a new domain with status "action required" is created and authenticated user is the owner
|
||||
assert domain.status == enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
assert domain.last_check_details == body_content_domain1
|
||||
assert domain.name == domain_name
|
||||
assert domain.accesses.filter(role="owner", user=user).exists()
|
||||
|
||||
|
||||
def test_api_mail_domains__create_authenticated__dimail_failure():
|
||||
@responses.activate
|
||||
def test_api_mail_domains__create_authenticated__dimail_failure(caplog):
|
||||
"""
|
||||
Despite a dimail failure for user and/or allow creation,
|
||||
an authenticated user should be able to create a mail domain
|
||||
and should automatically be added as owner of the newly created domain.
|
||||
"""
|
||||
caplog.set_level(logging.ERROR)
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
@@ -136,45 +181,60 @@ def test_api_mail_domains__create_authenticated__dimail_failure():
|
||||
|
||||
domain_name = "test.domain.fr"
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/domains/"),
|
||||
body=str(
|
||||
{
|
||||
"name": domain_name,
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/users/"),
|
||||
body=str(
|
||||
{
|
||||
"name": "request-user-sub",
|
||||
"is_admin": "false",
|
||||
"uuid": "user-uuid-on-dimail",
|
||||
"perms": [],
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/allows/"),
|
||||
body=str({"user": "request-user-sub", "domain": str(domain_name)}),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.post(
|
||||
"/api/v1.0/mail-domains/",
|
||||
{"name": domain_name, "context": "null", "features": ["webmail"]},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/domains/"),
|
||||
body=str(
|
||||
{
|
||||
"name": domain_name,
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/users/"),
|
||||
body=str(
|
||||
{
|
||||
"name": "request-user-sub",
|
||||
"is_admin": "false",
|
||||
"uuid": "user-uuid-on-dimail",
|
||||
"perms": [],
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/allows/"),
|
||||
body=str({"user": "request-user-sub", "domain": str(domain_name)}),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_error = {
|
||||
"status_code": status.HTTP_401_UNAUTHORIZED,
|
||||
"detail": "Not authorized",
|
||||
}
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain_name}/check/"),
|
||||
body=json.dumps(dimail_error),
|
||||
status=dimail_error["status_code"],
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/mail-domains/",
|
||||
{
|
||||
"name": domain_name,
|
||||
"context": "null",
|
||||
"features": ["webmail"],
|
||||
"support_email": f"support@{domain_name}",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
domain = models.MailDomain.objects.get()
|
||||
|
||||
# response is as expected
|
||||
@@ -187,15 +247,22 @@ def test_api_mail_domains__create_authenticated__dimail_failure():
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
"count_mailboxes": 0,
|
||||
"support_email": domain.support_email,
|
||||
"last_check_details": None,
|
||||
"action_required_details": {},
|
||||
"expected_config": None,
|
||||
}
|
||||
|
||||
# a new domain with status "failed" is created and authenticated user is the owner
|
||||
assert domain.status == enums.MailDomainStatusChoices.FAILED
|
||||
assert domain.name == domain_name
|
||||
assert domain.accesses.filter(role="owner", user=user).exists()
|
||||
assert caplog.records[0].levelname == "ERROR"
|
||||
assert "Not authorized" in caplog.records[0].message
|
||||
|
||||
|
||||
## SYNC TO DIMAIL
|
||||
@responses.activate
|
||||
def test_api_mail_domains__create_dimail_domain(caplog):
|
||||
"""
|
||||
Creating a domain should trigger a call to create a domain on dimail too.
|
||||
@@ -207,49 +274,65 @@ def test_api_mail_domains__create_dimail_domain(caplog):
|
||||
client.force_login(user)
|
||||
domain_name = "test.fr"
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsp = rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/domains/"),
|
||||
body=str(
|
||||
{
|
||||
"name": domain_name,
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/users/"),
|
||||
body=str(
|
||||
{
|
||||
"name": "request-user-sub",
|
||||
"is_admin": "false",
|
||||
"uuid": "user-uuid-on-dimail",
|
||||
"perms": [],
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/allows/"),
|
||||
body=str({"user": "request-user-sub", "domain": str(domain_name)}),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.post(
|
||||
"/api/v1.0/mail-domains/",
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/domains/"),
|
||||
body=str(
|
||||
{
|
||||
"name": domain_name,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/users/"),
|
||||
body=str(
|
||||
{
|
||||
"name": "request-user-sub",
|
||||
"is_admin": "false",
|
||||
"uuid": "user-uuid-on-dimail",
|
||||
"perms": [],
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/allows/"),
|
||||
body=str({"user": "request-user-sub", "domain": str(domain_name)}),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
body_content_domain1 = CHECK_DOMAIN_OK.copy()
|
||||
body_content_domain1["name"] = domain_name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain_name}/check/"),
|
||||
body=json.dumps(body_content_domain1),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain_name}/spec/"),
|
||||
body=json.dumps(DOMAIN_SPEC),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.post(
|
||||
"/api/v1.0/mail-domains/",
|
||||
{
|
||||
"name": domain_name,
|
||||
"support_email": f"support@{domain_name}",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert rsp.call_count == 1 # endpoint was called
|
||||
|
||||
# Logger
|
||||
assert len(caplog.records) == 4 # should be 3. Last empty info still here.
|
||||
@@ -267,6 +350,7 @@ def test_api_mail_domains__create_dimail_domain(caplog):
|
||||
)
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__no_creation_when_dimail_duplicate(caplog):
|
||||
"""No domain should be created when dimail returns a 409 conflict."""
|
||||
user = core_factories.UserFactory()
|
||||
@@ -278,27 +362,47 @@ def test_api_mail_domains__no_creation_when_dimail_duplicate(caplog):
|
||||
"status_code": status.HTTP_409_CONFLICT,
|
||||
"detail": "Domain already exists",
|
||||
}
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/users/"),
|
||||
body=str(
|
||||
{
|
||||
"name": "request-user-sub",
|
||||
"is_admin": "false",
|
||||
"uuid": "user-uuid-on-dimail",
|
||||
"perms": [],
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/allows/"),
|
||||
body=str({"user": "request-user-sub", "domain": str(domain_name)}),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/domains/"),
|
||||
body=str({"detail": dimail_error["detail"]}),
|
||||
status=dimail_error["status_code"],
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsp = rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/domains/"),
|
||||
body=str({"detail": dimail_error["detail"]}),
|
||||
status=dimail_error["status_code"],
|
||||
content_type="application/json",
|
||||
with pytest.raises(HTTPError):
|
||||
response = client.post(
|
||||
"/api/v1.0/mail-domains/",
|
||||
{
|
||||
"name": domain_name,
|
||||
"support_email": f"support@{domain_name}",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
with pytest.raises(HTTPError):
|
||||
response = client.post(
|
||||
"/api/v1.0/mail-domains/",
|
||||
{
|
||||
"name": domain_name,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert rsp.call_count == 1 # endpoint was called
|
||||
assert response.status_code == dimail_error["status_code"]
|
||||
assert response.json() == {"detail": dimail_error["detail"]}
|
||||
assert response.status_code == dimail_error["status_code"]
|
||||
assert response.json() == {"detail": dimail_error["detail"]}
|
||||
|
||||
# check logs
|
||||
record = caplog.records[0]
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
Tests for MailDomains API endpoint in People's mailbox manager app. Focus on "fetch" action.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories
|
||||
from mailbox_manager.tests.fixtures import dimail as dimail_fixtures
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__fetch_from_dimail__anonymous():
|
||||
"""
|
||||
Anonymous users should not be allowed to fetch a domain from dimail.
|
||||
"""
|
||||
client = APIClient()
|
||||
|
||||
domain = factories.MailDomainFactory()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/fetch/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__fetch_from_dimail__unrelated():
|
||||
"""
|
||||
Authenticated users shouldn't be allowed to fetch
|
||||
a domain from dimail if they are not an owner or admin.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
domain = factories.MailDomainFactory(
|
||||
status=enums.MailDomainStatusChoices.PENDING,
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/fetch/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
def test_api_mail_domains__fetch_from_dimail__viewer():
|
||||
"""
|
||||
Authenticated users shouldn't be allowed to fetch a domain from dimail
|
||||
if they are a viewer.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
access = factories.MailDomainAccessFactory(
|
||||
user=user,
|
||||
role=enums.MailDomainRoleChoices.VIEWER,
|
||||
)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/fetch/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
[
|
||||
enums.MailDomainRoleChoices.ADMIN,
|
||||
enums.MailDomainRoleChoices.OWNER,
|
||||
],
|
||||
)
|
||||
@responses.activate
|
||||
def test_api_mail_domains__fetch_from_dimail(role):
|
||||
"""
|
||||
Authenticated users should be allowed to fetch a domain
|
||||
from dimail if they are an owner or admin.
|
||||
"""
|
||||
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
domain = factories.MailDomainFactory(
|
||||
status=enums.MailDomainStatusChoices.PENDING,
|
||||
)
|
||||
factories.MailDomainAccessFactory(
|
||||
domain=domain,
|
||||
user=user,
|
||||
role=role,
|
||||
)
|
||||
|
||||
assert domain.expected_config is None
|
||||
assert domain.last_check_details is None
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
json=dimail_fixtures.CHECK_DOMAIN_OK,
|
||||
status=200,
|
||||
)
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/spec/"),
|
||||
json=dimail_fixtures.DOMAIN_SPEC,
|
||||
status=200,
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/fetch/",
|
||||
)
|
||||
|
||||
domain.refresh_from_db()
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"id": str(domain.id),
|
||||
"name": domain.name,
|
||||
"slug": domain.slug,
|
||||
"status": str(enums.MailDomainStatusChoices.ENABLED),
|
||||
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
"count_mailboxes": 0,
|
||||
"support_email": domain.support_email,
|
||||
"last_check_details": dimail_fixtures.CHECK_DOMAIN_OK,
|
||||
"action_required_details": {},
|
||||
"expected_config": dimail_fixtures.DOMAIN_SPEC,
|
||||
}
|
||||
assert domain.expected_config == dimail_fixtures.DOMAIN_SPEC
|
||||
assert domain.last_check_details == dimail_fixtures.CHECK_DOMAIN_OK
|
||||
assert domain.status == enums.MailDomainStatusChoices.ENABLED
|
||||
+130
-1
@@ -3,16 +3,19 @@ Tests for MailDomains API endpoint in People's mailbox manager app. Focus on "re
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import factories
|
||||
from mailbox_manager import enums, factories
|
||||
from mailbox_manager.tests.fixtures import dimail as dimail_fixtures
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__retrieve_anonymous():
|
||||
"""Anonymous users should not be allowed to retrieve a domain."""
|
||||
|
||||
@@ -23,8 +26,11 @@ def test_api_mail_domains__retrieve_anonymous():
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
# Verify no calls were made to dimail API
|
||||
assert len(responses.calls) == 0
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_domains__retrieve_non_existing():
|
||||
"""
|
||||
Authenticated users should have an explicit error when trying to retrive
|
||||
@@ -38,8 +44,11 @@ def test_api_domains__retrieve_non_existing():
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
# Verify no calls were made to dimail API
|
||||
assert len(responses.calls) == 0
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__retrieve_authenticated_unrelated():
|
||||
"""
|
||||
Authenticated users should not be allowed to retrieve a domain
|
||||
@@ -59,6 +68,7 @@ def test_api_mail_domains__retrieve_authenticated_unrelated():
|
||||
assert response.json() == {"detail": "No MailDomain matches the given query."}
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__retrieve_authenticated_related():
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a domain
|
||||
@@ -87,4 +97,123 @@ def test_api_mail_domains__retrieve_authenticated_related():
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
"count_mailboxes": 10,
|
||||
"support_email": domain.support_email,
|
||||
"last_check_details": None,
|
||||
"action_required_details": {},
|
||||
"expected_config": None,
|
||||
}
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__retrieve_authenticated_related_with_action_required():
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a domain
|
||||
to which they have access and which has actions required to be done.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
domain = factories.MailDomainFactory(
|
||||
status=enums.MailDomainStatusChoices.ACTION_REQUIRED,
|
||||
last_check_details=dimail_fixtures.CHECK_DOMAIN_BROKEN_EXTERNAL,
|
||||
)
|
||||
factories.MailDomainAccessFactory(domain=domain, user=user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"id": str(domain.id),
|
||||
"name": domain.name,
|
||||
"slug": domain.slug,
|
||||
"status": domain.status,
|
||||
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
"count_mailboxes": 0,
|
||||
"support_email": domain.support_email,
|
||||
"last_check_details": dimail_fixtures.CHECK_DOMAIN_BROKEN_EXTERNAL,
|
||||
"action_required_details": {
|
||||
"mx": "Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr., "
|
||||
"or je trouve example-fr.mail.protection.outlook.com.",
|
||||
},
|
||||
"expected_config": None,
|
||||
}
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__retrieve_authenticated_related_with_ok_status():
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a domain
|
||||
to which they have access and which has no actions required to be done.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
domain = factories.MailDomainEnabledFactory(
|
||||
last_check_details=dimail_fixtures.CHECK_DOMAIN_OK,
|
||||
)
|
||||
factories.MailDomainAccessFactory(domain=domain, user=user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"id": str(domain.id),
|
||||
"name": domain.name,
|
||||
"slug": domain.slug,
|
||||
"status": domain.status,
|
||||
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
"count_mailboxes": 0,
|
||||
"support_email": domain.support_email,
|
||||
"last_check_details": dimail_fixtures.CHECK_DOMAIN_OK,
|
||||
"action_required_details": {},
|
||||
"expected_config": None,
|
||||
}
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__retrieve_authenticated_related_with_failed_status():
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a domain
|
||||
to which they have access and which has failed status.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
domain = factories.MailDomainFactory(
|
||||
status=enums.MailDomainStatusChoices.FAILED,
|
||||
last_check_details=dimail_fixtures.CHECK_DOMAIN_BROKEN_INTERNAL,
|
||||
)
|
||||
factories.MailDomainAccessFactory(domain=domain, user=user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"id": str(domain.id),
|
||||
"name": domain.name,
|
||||
"slug": domain.slug,
|
||||
"status": domain.status,
|
||||
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
"count_mailboxes": 0,
|
||||
"support_email": domain.support_email,
|
||||
"last_check_details": dimail_fixtures.CHECK_DOMAIN_BROKEN_INTERNAL,
|
||||
"action_required_details": {},
|
||||
"expected_config": None,
|
||||
}
|
||||
|
||||
@@ -255,6 +255,25 @@ CHECK_DOMAIN_OK = {
|
||||
"cert": {"ok": True, "internal": True, "errors": []},
|
||||
}
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
DOMAIN_SPEC = [
|
||||
{"target": "", "type": "mx", "value": "mx.ox.numerique.gouv.fr."},
|
||||
{
|
||||
"target": "dimail._domainkey",
|
||||
"type": "txt",
|
||||
"value": "v=DKIM1; h=sha256; k=rsa; p=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
},
|
||||
{"target": "imap", "type": "cname", "value": "imap.ox.numerique.gouv.fr."},
|
||||
{"target": "smtp", "type": "cname", "value": "smtp.ox.numerique.gouv.fr."},
|
||||
{
|
||||
"target": "",
|
||||
"type": "txt",
|
||||
"value": "v=spf1 include:_spf.ox.numerique.gouv.fr -all",
|
||||
},
|
||||
{"target": "webmail", "type": "cname", "value": "webmail.ox.numerique.gouv.fr."},
|
||||
]
|
||||
|
||||
|
||||
## TOKEN
|
||||
|
||||
TOKEN_OK = json.dumps({"access_token": "token", "token_type": "bearer"})
|
||||
|
||||
@@ -18,6 +18,7 @@ from mailbox_manager import enums, factories, models
|
||||
from .fixtures.dimail import (
|
||||
CHECK_DOMAIN_BROKEN,
|
||||
CHECK_DOMAIN_OK,
|
||||
DOMAIN_SPEC,
|
||||
TOKEN_OK,
|
||||
response_mailbox_created,
|
||||
)
|
||||
@@ -150,3 +151,56 @@ def test_fetch_domain_status__should_switch_to_enabled_when_domain_ok(client):
|
||||
assert "Check domains done with success" in response.content.decode("utf-8")
|
||||
for mailbox in models.Mailbox.objects.filter(domain=domain1):
|
||||
assert mailbox.status == enums.MailboxStatusChoices.ENABLED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"domain_status",
|
||||
[
|
||||
enums.MailDomainStatusChoices.PENDING,
|
||||
enums.MailDomainStatusChoices.FAILED,
|
||||
enums.MailDomainStatusChoices.ENABLED,
|
||||
],
|
||||
)
|
||||
@responses.activate
|
||||
@pytest.mark.django_db
|
||||
def test_fetch_domain_expected_config(client, domain_status):
|
||||
"""Test admin action to fetch domain expected config from dimail."""
|
||||
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
|
||||
client.force_login(admin)
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
data = {
|
||||
"action": "fetch_domain_expected_config_from_dimail",
|
||||
"_selected_action": [domain.id],
|
||||
}
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/spec/"),
|
||||
body=json.dumps(DOMAIN_SPEC),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
url = reverse("admin:mailbox_manager_maildomain_changelist")
|
||||
response = client.post(url, data, follow=True)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
domain.refresh_from_db()
|
||||
assert domain.expected_config == DOMAIN_SPEC
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_fetch_domain_expected_config__should_not_fetch_for_disabled_domain(client):
|
||||
"""Test admin action to fetch domain expected config from dimail."""
|
||||
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
|
||||
client.force_login(admin)
|
||||
domain = factories.MailDomainFactory(status=enums.MailDomainStatusChoices.DISABLED)
|
||||
data = {
|
||||
"action": "fetch_domain_expected_config_from_dimail",
|
||||
"_selected_action": [domain.id],
|
||||
}
|
||||
url = reverse("admin:mailbox_manager_maildomain_changelist")
|
||||
response = client.post(url, data, follow=True)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
domain.refresh_from_db()
|
||||
assert domain.expected_config is None
|
||||
assert "Domains disabled are excluded from fetch" in response.content.decode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
@@ -194,11 +194,13 @@ def test_dimail__fetch_domain_status__switch_to_enabled(domain_status):
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ENABLED
|
||||
assert domain.last_check_details == body_content
|
||||
|
||||
# call again, should be ok
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ENABLED
|
||||
assert domain.last_check_details == body_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -217,12 +219,12 @@ def test_dimail__fetch_domain_status__switch_to_action_required(
|
||||
"""Domains should be in status action required when dimail check
|
||||
returns broken status for external checks."""
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
body_content = CHECK_DOMAIN_BROKEN_EXTERNAL.copy()
|
||||
body_content["name"] = domain.name
|
||||
body_domain_broken = CHECK_DOMAIN_BROKEN_EXTERNAL.copy()
|
||||
body_domain_broken["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
body=json.dumps(body_domain_broken),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -230,21 +232,23 @@ def test_dimail__fetch_domain_status__switch_to_action_required(
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
assert domain.last_check_details == body_domain_broken
|
||||
|
||||
# Support team fixes their part of the problem
|
||||
# Now domain is OK again
|
||||
body_content = CHECK_DOMAIN_OK.copy()
|
||||
body_content["name"] = domain.name
|
||||
body_domain_ok = CHECK_DOMAIN_OK.copy()
|
||||
body_domain_ok["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
body=json.dumps(body_domain_ok),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ENABLED
|
||||
assert domain.last_check_details == body_domain_ok
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -261,12 +265,12 @@ def test_dimail__fetch_domain_status__switch_to_failed(domain_status):
|
||||
for only internal checks dispite a fix call."""
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
# nothing can be done by support team, domain should be in failed
|
||||
body_content = CHECK_DOMAIN_BROKEN_INTERNAL.copy()
|
||||
body_content["name"] = domain.name
|
||||
body_domain_broken = CHECK_DOMAIN_BROKEN_INTERNAL.copy()
|
||||
body_domain_broken["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
body=json.dumps(body_domain_broken),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -274,7 +278,7 @@ def test_dimail__fetch_domain_status__switch_to_failed(domain_status):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/fix/"),
|
||||
body=json.dumps(body_content),
|
||||
body=json.dumps(body_domain_broken),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -282,6 +286,7 @@ def test_dimail__fetch_domain_status__switch_to_failed(domain_status):
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.FAILED
|
||||
assert domain.last_check_details == body_domain_broken
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -298,12 +303,12 @@ def test_dimail__fetch_domain_status__full_fix_scenario(domain_status):
|
||||
after a fix call."""
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
# with all checks KO, domain should be in action required
|
||||
body_content = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_content["name"] = domain.name
|
||||
body_domain_broken = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_domain_broken["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
body=json.dumps(body_domain_broken),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -311,27 +316,28 @@ def test_dimail__fetch_domain_status__full_fix_scenario(domain_status):
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
assert domain.last_check_details == body_domain_broken
|
||||
|
||||
# We assume that the support has fixed their part.
|
||||
# So now dimail returns OK for external checks but still KO for internal checks.
|
||||
# A call to dimail fix endpoint is necessary and will be done by
|
||||
# the fetch_domain_status call
|
||||
body_content = CHECK_DOMAIN_BROKEN_INTERNAL.copy()
|
||||
body_content["name"] = domain.name
|
||||
body_domain_broken_internal = CHECK_DOMAIN_BROKEN_INTERNAL.copy()
|
||||
body_domain_broken_internal["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
body=json.dumps(body_domain_broken_internal),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
# the endpoint fix is called and returns OK. Hooray!
|
||||
body_content = CHECK_DOMAIN_OK.copy()
|
||||
body_content["name"] = domain.name
|
||||
body_domain_ok = CHECK_DOMAIN_OK.copy()
|
||||
body_domain_ok["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/fix/"),
|
||||
body=json.dumps(body_content),
|
||||
body=json.dumps(body_domain_ok),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -339,6 +345,7 @@ def test_dimail__fetch_domain_status__full_fix_scenario(domain_status):
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ENABLED
|
||||
assert domain.last_check_details == body_domain_ok
|
||||
|
||||
|
||||
def test_dimail__enable_pending_mailboxes(caplog):
|
||||
@@ -391,7 +398,7 @@ def test_dimail__enable_pending_mailboxes(caplog):
|
||||
)
|
||||
assert (
|
||||
caplog.records[2].message
|
||||
== f"Information for mailbox mock@{domain.name} sent to {mailbox1.secondary_email}."
|
||||
== f"Information for mailbox mock@{domain.name} sent to {mailbox2.secondary_email}."
|
||||
)
|
||||
assert (
|
||||
caplog.records[4].message
|
||||
@@ -399,5 +406,5 @@ def test_dimail__enable_pending_mailboxes(caplog):
|
||||
)
|
||||
assert (
|
||||
caplog.records[5].message
|
||||
== f"Information for mailbox mock@{domain.name} sent to {mailbox2.secondary_email}."
|
||||
== f"Information for mailbox mock@{domain.name} sent to {mailbox1.secondary_email}."
|
||||
)
|
||||
|
||||
@@ -418,12 +418,20 @@ class DimailAPIClient:
|
||||
|
||||
def check_domain(self, domain):
|
||||
"""Send a request to dimail to check domain health."""
|
||||
response = session.get(
|
||||
f"{self.API_URL}/domains/{domain.name}/check/",
|
||||
headers={"Authorization": f"Basic {self.API_CREDENTIALS}"},
|
||||
verify=True,
|
||||
timeout=10,
|
||||
)
|
||||
try:
|
||||
response = session.get(
|
||||
f"{self.API_URL}/domains/{domain.name}/check/",
|
||||
headers={"Authorization": f"Basic {self.API_CREDENTIALS}"},
|
||||
verify=True,
|
||||
timeout=10,
|
||||
)
|
||||
except requests.exceptions.ConnectionError as error:
|
||||
logger.error(
|
||||
"Connection error while trying to reach %s.",
|
||||
self.API_URL,
|
||||
exc_info=error,
|
||||
)
|
||||
raise error
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
return response.json()
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
@@ -448,43 +456,61 @@ class DimailAPIClient:
|
||||
def fetch_domain_status(self, domain):
|
||||
"""Send a request to check and update status of a domain."""
|
||||
dimail_response = self.check_domain(domain)
|
||||
dimail_state = dimail_response["state"]
|
||||
# if domain is not enabled and dimail returns ok status, enable it
|
||||
if (
|
||||
domain.status != enums.MailDomainStatusChoices.ENABLED
|
||||
and dimail_state == "ok"
|
||||
):
|
||||
self.enable_pending_mailboxes(domain)
|
||||
domain.status = enums.MailDomainStatusChoices.ENABLED
|
||||
domain.save()
|
||||
# if dimail returns broken status, we need to extract external and internal checks
|
||||
# and manage the case where the domain has to be fixed by support
|
||||
elif dimail_state == "broken":
|
||||
external_checks = self._get_dimail_checks(dimail_response, internal=False)
|
||||
internal_checks = self._get_dimail_checks(dimail_response, internal=True)
|
||||
# manage the case where the domain has to be fixed by support
|
||||
if not all(external_checks.values()):
|
||||
domain.status = enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
if dimail_response:
|
||||
dimail_state = dimail_response["state"]
|
||||
# if domain is not enabled and dimail returns ok status, enable it
|
||||
if (
|
||||
domain.status != enums.MailDomainStatusChoices.ENABLED
|
||||
and dimail_state == "ok"
|
||||
):
|
||||
self.enable_pending_mailboxes(domain)
|
||||
domain.status = enums.MailDomainStatusChoices.ENABLED
|
||||
domain.last_check_details = dimail_response
|
||||
domain.save()
|
||||
# if all external checks are ok but not internal checks, we need to fix internal checks
|
||||
elif all(external_checks.values()) and not all(internal_checks.values()):
|
||||
# a call to fix endpoint is needed because all external checks are ok
|
||||
dimail_response = self.fix_domain(domain)
|
||||
# we need to check again if all internal and external checks are ok
|
||||
# if dimail returns broken status, we need to extract external and internal checks
|
||||
# and manage the case where the domain has to be fixed by support
|
||||
elif dimail_state == "broken":
|
||||
external_checks = self._get_dimail_checks(
|
||||
dimail_response, internal=False
|
||||
)
|
||||
internal_checks = self._get_dimail_checks(
|
||||
dimail_response, internal=True
|
||||
)
|
||||
if all(external_checks.values()) and all(internal_checks.values()):
|
||||
domain.status = enums.MailDomainStatusChoices.ENABLED
|
||||
# manage the case where the domain has to be fixed by support
|
||||
if not all(external_checks.values()):
|
||||
domain.status = enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
domain.last_check_details = dimail_response
|
||||
domain.save()
|
||||
# if all external checks are ok but not internal checks, we need to fix
|
||||
# internal checks
|
||||
elif all(external_checks.values()) and not all(
|
||||
internal_checks.values()
|
||||
):
|
||||
domain.status = enums.MailDomainStatusChoices.FAILED
|
||||
domain.save()
|
||||
# a call to fix endpoint is needed because all external checks are ok
|
||||
dimail_response = self.fix_domain(domain)
|
||||
# we need to check again if all internal and external checks are ok
|
||||
external_checks = self._get_dimail_checks(
|
||||
dimail_response, internal=False
|
||||
)
|
||||
internal_checks = self._get_dimail_checks(
|
||||
dimail_response, internal=True
|
||||
)
|
||||
if all(external_checks.values()) and all(internal_checks.values()):
|
||||
domain.status = enums.MailDomainStatusChoices.ENABLED
|
||||
domain.last_check_details = dimail_response
|
||||
domain.save()
|
||||
elif all(external_checks.values()) and not all(
|
||||
internal_checks.values()
|
||||
):
|
||||
domain.status = enums.MailDomainStatusChoices.FAILED
|
||||
domain.last_check_details = dimail_response
|
||||
domain.save()
|
||||
|
||||
# if no health check data is stored on the domain, we store it now
|
||||
if not domain.last_check_details:
|
||||
domain.last_check_details = dimail_response
|
||||
domain.save()
|
||||
|
||||
return dimail_response
|
||||
|
||||
def _get_dimail_checks(self, dimail_response, internal: bool):
|
||||
@@ -494,3 +520,48 @@ class DimailAPIClient:
|
||||
if isinstance(value, dict) and value.get("internal") is internal
|
||||
}
|
||||
return {key: value.get("ok", False) for key, value in checks.items()}
|
||||
|
||||
def fetch_domain_expected_config(self, domain):
|
||||
"""Send a request to dimail to get domain specification for DNS configuration."""
|
||||
try:
|
||||
response = session.get(
|
||||
f"{self.API_URL}/domains/{domain.name}/spec/",
|
||||
headers={"Authorization": f"Basic {self.API_CREDENTIALS}"},
|
||||
verify=True,
|
||||
timeout=10,
|
||||
)
|
||||
except requests.exceptions.ConnectionError as error:
|
||||
logger.exception(
|
||||
"Connection error while trying to reach %s.",
|
||||
self.API_URL,
|
||||
exc_info=error,
|
||||
)
|
||||
return []
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
# format the response to log an error if api response changed
|
||||
try:
|
||||
dimail_expected_config = [
|
||||
{
|
||||
"target": item["target"],
|
||||
"type": item["type"],
|
||||
"value": item["value"],
|
||||
}
|
||||
for item in response.json()
|
||||
]
|
||||
domain.expected_config = dimail_expected_config
|
||||
domain.save()
|
||||
return dimail_expected_config
|
||||
except KeyError as error:
|
||||
logger.exception(
|
||||
"[DIMAIL] spec expected response format changed: %s",
|
||||
error,
|
||||
)
|
||||
return []
|
||||
else:
|
||||
logger.exception(
|
||||
"[DIMAIL] unexpected error : %s %s",
|
||||
response.status_code,
|
||||
response.content,
|
||||
exc_info=False,
|
||||
)
|
||||
return []
|
||||
|
||||
@@ -323,6 +323,42 @@ class Base(Configuration):
|
||||
SESSION_CACHE_ALIAS = "default"
|
||||
SESSION_COOKIE_AGE = 60 * 60 * 12 # 12 hours to match Agent Connect
|
||||
|
||||
# Python loggers configuration (and env var overrides)
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "{asctime} {name} {levelname} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "simple",
|
||||
},
|
||||
},
|
||||
# Override root logger to send it to console
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
"INFO", environ_name="LOGGING_LEVEL_LOGGERS_ROOT", environ_prefix=None
|
||||
),
|
||||
},
|
||||
"loggers": {
|
||||
"core": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
"INFO",
|
||||
environ_name="LOGGING_LEVEL_LOGGERS_APP",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# OIDC - Authorization Code Flow
|
||||
OIDC_CREATE_USER = values.BooleanValue(
|
||||
default=True,
|
||||
@@ -465,6 +501,21 @@ class Base(Configuration):
|
||||
environ_name="MAIL_PROVISIONING_API_CREDENTIALS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
DNS_PROVISIONING_API_URL = values.Value(
|
||||
default="https://api.scaleway.com",
|
||||
environ_name="DNS_PROVISIONING_API_URL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
DNS_PROVISIONING_RESOURCE_ID = values.Value(
|
||||
default=None,
|
||||
environ_name="DNS_PROVISIONING_RESOURCE_ID",
|
||||
environ_prefix=None,
|
||||
)
|
||||
DNS_PROVISIONING_API_CREDENTIALS = values.Value(
|
||||
default=None,
|
||||
environ_name="DNS_PROVISIONING_API_CREDENTIALS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Organizations
|
||||
ORGANIZATION_REGISTRATION_ID_VALIDATORS = json.loads(
|
||||
@@ -602,48 +653,11 @@ class Development(Base):
|
||||
|
||||
USE_SWAGGER = True
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "{asctime} {name} {levelname} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "simple",
|
||||
},
|
||||
},
|
||||
# Override root logger to send it to console
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
"INFO", environ_name="LOGGING_LEVEL_LOGGERS_ROOT", environ_prefix=None
|
||||
),
|
||||
},
|
||||
"loggers": {
|
||||
"core": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
"INFO",
|
||||
environ_name="LOGGING_LEVEL_LOGGERS_APP",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# this is a dev credentials for mail provisioning API
|
||||
MAIL_PROVISIONING_API_CREDENTIALS = "bGFfcmVnaWU6cGFzc3dvcmQ="
|
||||
|
||||
OIDC_ORGANIZATION_REGISTRATION_ID_FIELD = "siret"
|
||||
|
||||
ORGANIZATION_PLUGINS = ["plugins.organizations.NameFromSiretOrganizationPlugin"]
|
||||
|
||||
def __init__(self):
|
||||
"""In dev, force installs needed for Swagger API."""
|
||||
# pylint: disable=invalid-name
|
||||
@@ -691,8 +705,6 @@ class Test(Base):
|
||||
|
||||
OIDC_ORGANIZATION_REGISTRATION_ID_FIELD = "siret"
|
||||
|
||||
ORGANIZATION_PLUGINS = ["plugins.organizations.NameFromSiretOrganizationPlugin"]
|
||||
|
||||
ORGANIZATION_REGISTRATION_ID_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.core.validators.RegexValidator",
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
"""Organization related plugins."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.text import slugify
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
from core.plugins.base import BaseOrganizationPlugin
|
||||
|
||||
from mailbox_manager.enums import MailDomainRoleChoices
|
||||
from mailbox_manager.models import MailDomain, MailDomainAccess
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -27,14 +34,8 @@ class NameFromSiretOrganizationPlugin(BaseOrganizationPlugin):
|
||||
def get_organization_name_from_results(data, siret):
|
||||
"""Return the organization name from the results of a SIRET search."""
|
||||
for result in data["results"]:
|
||||
is_commune = (
|
||||
result.get("nature_juridique") == "7210"
|
||||
) # INSEE code for commune
|
||||
for organization in result["matching_etablissements"]:
|
||||
if organization.get("siret") == siret:
|
||||
if is_commune:
|
||||
return organization["libelle_commune"].title()
|
||||
|
||||
store_signs = organization.get("liste_enseignes") or []
|
||||
if store_signs:
|
||||
return store_signs[0].title()
|
||||
@@ -76,3 +77,234 @@ class NameFromSiretOrganizationPlugin(BaseOrganizationPlugin):
|
||||
organization.name = name
|
||||
organization.save(update_fields=["name", "updated_at"])
|
||||
logger.info("Organization %s name updated to %s", organization, name)
|
||||
|
||||
def run_after_grant_access(self, organization_access):
|
||||
"""After granting an organization access, we don't need to do anything."""
|
||||
|
||||
|
||||
class ApiCall:
|
||||
"""Encapsulates a call to an external API"""
|
||||
|
||||
inputs: dict = {}
|
||||
method: str = "GET"
|
||||
base: str = ""
|
||||
url: str = ""
|
||||
params: dict = {}
|
||||
headers: dict = {}
|
||||
response_data = None
|
||||
|
||||
def execute(self):
|
||||
"""Call the specified API endpoint with supplied parameters and record response"""
|
||||
if self.method in ("POST", "PATCH"):
|
||||
response = requests.request(
|
||||
method=self.method,
|
||||
url=f"{self.base}/{self.url}",
|
||||
json=self.params,
|
||||
headers=self.headers,
|
||||
timeout=5,
|
||||
)
|
||||
else:
|
||||
response = requests.request(
|
||||
method=self.method,
|
||||
url=f"{self.base}/{self.url}",
|
||||
params=self.params,
|
||||
headers=self.headers,
|
||||
timeout=5,
|
||||
)
|
||||
self.response_data = response.json()
|
||||
logger.info(
|
||||
"API call: %s %s %s %s",
|
||||
self.method,
|
||||
self.url,
|
||||
self.params,
|
||||
self.response_data,
|
||||
)
|
||||
|
||||
|
||||
class CommuneCreation(BaseOrganizationPlugin):
|
||||
"""
|
||||
This plugin handles setup tasks for French communes.
|
||||
"""
|
||||
|
||||
_api_url = "https://recherche-entreprises.api.gouv.fr/search?q={siret}"
|
||||
|
||||
def get_organization_name_from_results(self, data, siret):
|
||||
"""Return the organization name from the results of a SIRET search."""
|
||||
for result in data["results"]:
|
||||
nature = "nature_juridique"
|
||||
commune = nature in result and result[nature] == "7210"
|
||||
if commune:
|
||||
return result["siege"]["libelle_commune"].title()
|
||||
|
||||
logger.warning("Not a commune: SIRET %s", siret)
|
||||
return None
|
||||
|
||||
def dns_call(self, spec):
|
||||
"""Call to add a DNS record"""
|
||||
zone_name = self.zone_name(spec.inputs["name"])
|
||||
|
||||
records = [
|
||||
{
|
||||
"name": item["target"],
|
||||
"type": item["type"].upper(),
|
||||
"data": item["value"],
|
||||
"ttl": 3600,
|
||||
}
|
||||
for item in spec.response_data
|
||||
]
|
||||
result = ApiCall()
|
||||
result.method = "PATCH"
|
||||
result.base = "https://api.scaleway.com"
|
||||
result.url = f"/domain/v2beta1/dns-zones/{zone_name}/records"
|
||||
result.params = {"changes": [{"add": {"records": records}}]}
|
||||
result.headers = {"X-Auth-Token": settings.DNS_PROVISIONING_API_CREDENTIALS}
|
||||
return result
|
||||
|
||||
def normalize_name(self, name: str) -> str:
|
||||
"""Map the name to a standard form"""
|
||||
name = re.sub("'", "-", name)
|
||||
return slugify(name)
|
||||
|
||||
def zone_name(self, name: str) -> str:
|
||||
"""Derive the zone name from the commune name"""
|
||||
normalized = self.normalize_name(name)
|
||||
return f"{normalized}.collectivite.fr"
|
||||
|
||||
def complete_commune_creation(self, name: str) -> ApiCall:
|
||||
"""Specify the tasks to be completed after a commune is created."""
|
||||
inputs = {"name": self.normalize_name(name)}
|
||||
|
||||
create_zone = ApiCall()
|
||||
create_zone.method = "POST"
|
||||
create_zone.base = "https://api.scaleway.com"
|
||||
create_zone.url = "/domain/v2beta1/dns-zones"
|
||||
create_zone.params = {
|
||||
"project_id": settings.DNS_PROVISIONING_RESOURCE_ID,
|
||||
"domain": "collectivite.fr",
|
||||
"subdomain": inputs["name"],
|
||||
}
|
||||
create_zone.headers = {
|
||||
"X-Auth-Token": settings.DNS_PROVISIONING_API_CREDENTIALS
|
||||
}
|
||||
|
||||
zone_name = self.zone_name(inputs["name"])
|
||||
|
||||
create_domain = ApiCall()
|
||||
create_domain.method = "POST"
|
||||
create_domain.base = settings.MAIL_PROVISIONING_API_URL
|
||||
create_domain.url = "/domains/"
|
||||
create_domain.params = {
|
||||
"name": zone_name,
|
||||
"delivery": "virtual",
|
||||
"features": ["webmail", "mailbox"],
|
||||
"context_name": zone_name,
|
||||
}
|
||||
create_domain.headers = {
|
||||
"Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
}
|
||||
|
||||
spec_domain = ApiCall()
|
||||
spec_domain.inputs = inputs
|
||||
spec_domain.base = settings.MAIL_PROVISIONING_API_URL
|
||||
spec_domain.url = f"/domains/{zone_name}/spec"
|
||||
spec_domain.headers = {
|
||||
"Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
}
|
||||
|
||||
return [create_zone, create_domain, spec_domain]
|
||||
|
||||
def complete_zone_creation(self, spec_call):
|
||||
"""Specify the tasks to be performed to set up the zone."""
|
||||
return self.dns_call(spec_call)
|
||||
|
||||
def run_after_create(self, organization):
|
||||
"""After creating an organization, update the organization name."""
|
||||
logger.info("In CommuneCreation")
|
||||
if not organization.registration_id_list:
|
||||
# No registration ID to convert...
|
||||
return
|
||||
|
||||
# In the nominal case, there is only one registration ID because
|
||||
# the organization has been created from it.
|
||||
try:
|
||||
# Retry logic as the API may be rate limited
|
||||
s = requests.Session()
|
||||
retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[429])
|
||||
s.mount("https://", HTTPAdapter(max_retries=retries))
|
||||
|
||||
siret = organization.registration_id_list[0]
|
||||
response = s.get(self._api_url.format(siret=siret), timeout=10)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
name = self.get_organization_name_from_results(data, siret)
|
||||
# Not a commune ?
|
||||
if not name:
|
||||
return
|
||||
except requests.RequestException as exc:
|
||||
logger.exception("%s: Unable to fetch organization name from SIRET", exc)
|
||||
return
|
||||
|
||||
organization.name = name
|
||||
organization.save(update_fields=["name", "updated_at"])
|
||||
logger.info("Organization %s name updated to %s", organization, name)
|
||||
|
||||
zone_name = self.zone_name(name)
|
||||
support = "support-regie@numerique.gouv.fr"
|
||||
MailDomain.objects.get_or_create(name=zone_name, support_email=support)
|
||||
|
||||
# Compute and execute the rest of the process
|
||||
tasks = self.complete_commune_creation(name)
|
||||
for task in tasks:
|
||||
task.execute()
|
||||
last_task = self.complete_zone_creation(tasks[-1])
|
||||
last_task.execute()
|
||||
|
||||
def complete_grant_access(self, sub, zone_name):
|
||||
"""Specify the tasks to be completed after making a user admin"""
|
||||
create_user = ApiCall()
|
||||
create_user.method = "POST"
|
||||
create_user.base = settings.MAIL_PROVISIONING_API_URL
|
||||
create_user.url = "/users/"
|
||||
create_user.params = {
|
||||
"name": sub,
|
||||
"password": "no",
|
||||
"is_admin": False,
|
||||
"perms": [],
|
||||
}
|
||||
create_user.headers = {
|
||||
"Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
}
|
||||
|
||||
grant_access = ApiCall()
|
||||
grant_access.method = "POST"
|
||||
grant_access.base = settings.MAIL_PROVISIONING_API_URL
|
||||
grant_access.url = "/allows/"
|
||||
grant_access.params = {
|
||||
"user": sub,
|
||||
"domain": zone_name,
|
||||
}
|
||||
grant_access.headers = {
|
||||
"Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
}
|
||||
|
||||
return [create_user, grant_access]
|
||||
|
||||
def run_after_grant_access(self, organization_access):
|
||||
"""After granting an organization access, check for needed domain access grant."""
|
||||
orga = organization_access.organization
|
||||
user = organization_access.user
|
||||
zone_name = self.zone_name(orga.name)
|
||||
|
||||
try:
|
||||
domain = MailDomain.objects.get(name=zone_name)
|
||||
except MailDomain.DoesNotExist:
|
||||
domain = None
|
||||
|
||||
if domain:
|
||||
MailDomainAccess.objects.create(
|
||||
domain=domain, user=user, role=MailDomainRoleChoices.OWNER
|
||||
)
|
||||
|
||||
tasks = self.complete_grant_access(user.sub, zone_name)
|
||||
for task in tasks:
|
||||
task.execute()
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
"""Tests for the CommuneCreation plugin."""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
|
||||
from plugins.organizations import ApiCall, CommuneCreation
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_extract_name_from_org_data_when_commune():
|
||||
"""Test the name is extracted correctly for a French commune."""
|
||||
data = {
|
||||
"results": [
|
||||
{
|
||||
"nom_complet": "COMMUNE DE VARZY",
|
||||
"nom_raison_sociale": "COMMUNE DE VARZY",
|
||||
"siege": {
|
||||
"libelle_commune": "VARZY",
|
||||
"liste_enseignes": ["MAIRIE"],
|
||||
"siret": "21580304000017",
|
||||
},
|
||||
"nature_juridique": "7210",
|
||||
"matching_etablissements": [
|
||||
{
|
||||
"siret": "21580304000017",
|
||||
"libelle_commune": "VARZY",
|
||||
"liste_enseignes": ["MAIRIE"],
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
plugin = CommuneCreation()
|
||||
name = plugin.get_organization_name_from_results(data, "21580304000017")
|
||||
assert name == "Varzy"
|
||||
|
||||
|
||||
def test_api_call_execution():
|
||||
"""Test that calling execute() faithfully executes the API call"""
|
||||
task = ApiCall()
|
||||
task.method = "POST"
|
||||
task.base = "https://some_host"
|
||||
task.url = "some_url"
|
||||
task.params = {"some_key": "some_value"}
|
||||
task.headers = {"Some-Header": "Some-Header-Value"}
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
url="https://some_host/some_url",
|
||||
body='{"some_key": "some_value"}',
|
||||
content_type="application/json",
|
||||
headers={"Some-Header": "Some-Header-Value"},
|
||||
)
|
||||
|
||||
task.execute()
|
||||
|
||||
|
||||
def test_tasks_on_commune_creation_include_zone_creation():
|
||||
"""Test the first task in commune creation: creating the DNS sub-zone"""
|
||||
plugin = CommuneCreation()
|
||||
name = "Varzy"
|
||||
|
||||
tasks = plugin.complete_commune_creation(name)
|
||||
|
||||
assert tasks[0].base == "https://api.scaleway.com"
|
||||
assert tasks[0].url == "/domain/v2beta1/dns-zones"
|
||||
assert tasks[0].method == "POST"
|
||||
assert tasks[0].params == {
|
||||
"project_id": settings.DNS_PROVISIONING_RESOURCE_ID,
|
||||
"domain": "collectivite.fr",
|
||||
"subdomain": "varzy",
|
||||
}
|
||||
assert tasks[0].headers["X-Auth-Token"] == settings.DNS_PROVISIONING_API_CREDENTIALS
|
||||
|
||||
|
||||
def test_tasks_on_commune_creation_include_dimail_domain_creation():
|
||||
"""Test the second task in commune creation: creating the domain in Dimail"""
|
||||
plugin = CommuneCreation()
|
||||
name = "Merlaut"
|
||||
|
||||
tasks = plugin.complete_commune_creation(name)
|
||||
|
||||
assert tasks[1].base == settings.MAIL_PROVISIONING_API_URL
|
||||
assert tasks[1].url == "/domains/"
|
||||
assert tasks[1].method == "POST"
|
||||
assert tasks[1].params == {
|
||||
"name": "merlaut.collectivite.fr",
|
||||
"delivery": "virtual",
|
||||
"features": ["webmail", "mailbox"],
|
||||
"context_name": "merlaut.collectivite.fr",
|
||||
}
|
||||
assert (
|
||||
tasks[1].headers["Authorization"]
|
||||
== f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
)
|
||||
|
||||
|
||||
def test_tasks_on_commune_creation_include_fetching_spec():
|
||||
"""Test the third task in commune creation: asking Dimail for the spec"""
|
||||
plugin = CommuneCreation()
|
||||
name = "Loc-Eguiner"
|
||||
|
||||
tasks = plugin.complete_commune_creation(name)
|
||||
|
||||
assert tasks[2].base == settings.MAIL_PROVISIONING_API_URL
|
||||
assert tasks[2].url == "/domains/loc-eguiner.collectivite.fr/spec"
|
||||
assert tasks[2].method == "GET"
|
||||
assert (
|
||||
tasks[2].headers["Authorization"]
|
||||
== f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
)
|
||||
|
||||
|
||||
def test_tasks_on_commune_creation_include_dns_records():
|
||||
"""Test the next several tasks in commune creation: creating records"""
|
||||
plugin = CommuneCreation()
|
||||
name = "Abidos"
|
||||
|
||||
spec_response = [
|
||||
{"target": "", "type": "mx", "value": "mx.dev.ox.numerique.gouv.fr."},
|
||||
{
|
||||
"target": "dimail._domainkey",
|
||||
"type": "txt",
|
||||
"value": "v=DKIM1; h=sha256; k=rsa; p=MIICIjANB<truncated>AAQ==",
|
||||
},
|
||||
{"target": "imap", "type": "cname", "value": "imap.dev.ox.numerique.gouv.fr."},
|
||||
{"target": "smtp", "type": "cname", "value": "smtp.dev.ox.numerique.gouv.fr."},
|
||||
{
|
||||
"target": "",
|
||||
"type": "txt",
|
||||
"value": "v=spf1 include:_spf.dev.ox.numerique.gouv.fr -all",
|
||||
},
|
||||
{
|
||||
"target": "webmail",
|
||||
"type": "cname",
|
||||
"value": "webmail.dev.ox.numerique.gouv.fr.",
|
||||
},
|
||||
]
|
||||
|
||||
tasks = plugin.complete_commune_creation(name)
|
||||
tasks[2].response_data = spec_response
|
||||
|
||||
expected = {
|
||||
"changes": [
|
||||
{
|
||||
"add": {
|
||||
"records": [
|
||||
{
|
||||
"name": item["target"],
|
||||
"type": item["type"].upper(),
|
||||
"data": item["value"],
|
||||
"ttl": 3600,
|
||||
}
|
||||
for item in spec_response
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
zone_call = plugin.complete_zone_creation(tasks[2])
|
||||
assert zone_call.params == expected
|
||||
assert zone_call.url == "/domain/v2beta1/dns-zones/abidos.collectivite.fr/records"
|
||||
assert (
|
||||
zone_call.headers["X-Auth-Token"] == settings.DNS_PROVISIONING_API_CREDENTIALS
|
||||
)
|
||||
|
||||
|
||||
def test_tasks_on_grant_access():
|
||||
"""Test the final tasks after making user admin of an org"""
|
||||
plugin = CommuneCreation()
|
||||
|
||||
tasks = plugin.complete_grant_access("some-sub", "mezos.collectivite.fr")
|
||||
|
||||
assert tasks[0].base == settings.MAIL_PROVISIONING_API_URL
|
||||
assert tasks[0].url == "/users/"
|
||||
assert tasks[0].method == "POST"
|
||||
assert tasks[0].params == {
|
||||
"name": "some-sub",
|
||||
"password": "no",
|
||||
"is_admin": False,
|
||||
"perms": [],
|
||||
}
|
||||
assert (
|
||||
tasks[0].headers["Authorization"]
|
||||
== f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
)
|
||||
|
||||
assert tasks[1].base == settings.MAIL_PROVISIONING_API_URL
|
||||
assert tasks[1].url == "/allows/"
|
||||
assert tasks[1].method == "POST"
|
||||
assert tasks[1].params == {
|
||||
"user": "some-sub",
|
||||
"domain": "mezos.collectivite.fr",
|
||||
}
|
||||
assert (
|
||||
tasks[1].headers["Authorization"]
|
||||
== f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_name():
|
||||
"""Test name normalization"""
|
||||
plugin = CommuneCreation()
|
||||
assert plugin.normalize_name("Asnières-sur-Saône") == "asnieres-sur-saone"
|
||||
assert plugin.normalize_name("Bâgé-le-Châtel") == "bage-le-chatel"
|
||||
assert plugin.normalize_name("Courçais") == "courcais"
|
||||
assert plugin.normalize_name("Moÿ-de-l'Aisne") == "moy-de-l-aisne"
|
||||
assert plugin.normalize_name("Salouël") == "salouel"
|
||||
assert (
|
||||
plugin.normalize_name("Bors (Canton de Tude-et-Lavalette)")
|
||||
== "bors-canton-de-tude-et-lavalette"
|
||||
)
|
||||
|
||||
|
||||
def test_zone_name():
|
||||
"""Test transforming a commune name to a sub-zone of collectivite.fr"""
|
||||
plugin = CommuneCreation()
|
||||
assert plugin.zone_name("Bâgé-le-Châtel") == "bage-le-chatel.collectivite.fr"
|
||||
@@ -6,8 +6,6 @@ import responses
|
||||
from core.models import Organization
|
||||
from core.plugins.loader import get_organization_plugins
|
||||
|
||||
from plugins.organizations import NameFromSiretOrganizationPlugin
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@@ -139,37 +137,6 @@ def test_organization_plugins_run_after_create_name_already_set(
|
||||
assert organization.name == "Magic WOW"
|
||||
|
||||
|
||||
def test_extract_name_from_org_data_when_commune(
|
||||
organization_plugins_settings,
|
||||
):
|
||||
"""Test the name is extracted correctly for a French commune."""
|
||||
data = {
|
||||
"results": [
|
||||
{
|
||||
"nom_complet": "COMMUNE DE VARZY",
|
||||
"nom_raison_sociale": "COMMUNE DE VARZY",
|
||||
"siege": {
|
||||
"libelle_commune": "VARZY",
|
||||
"liste_enseignes": ["MAIRIE"],
|
||||
"siret": "21580304000017",
|
||||
},
|
||||
"nature_juridique": "7210",
|
||||
"matching_etablissements": [
|
||||
{
|
||||
"siret": "21580304000017",
|
||||
"libelle_commune": "VARZY",
|
||||
"liste_enseignes": ["MAIRIE"],
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
plugin = NameFromSiretOrganizationPlugin()
|
||||
name = plugin.get_organization_name_from_results(data, "21580304000017")
|
||||
assert name == "Varzy"
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_organization_plugins_run_after_create_no_list_enseignes(
|
||||
organization_plugins_settings,
|
||||
|
||||
+20
-14
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "people"
|
||||
version = "1.10.1"
|
||||
version = "1.12.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,19 +25,19 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"boto3==1.36.11",
|
||||
"boto3==1.36.21",
|
||||
"Brotli==1.1.0",
|
||||
"celery[redis]==5.4.0",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.6.0",
|
||||
"django-cors-headers==4.7.0",
|
||||
"django-countries==7.6.1",
|
||||
"django-parler==2.3",
|
||||
"django-treebeard==4.7.1",
|
||||
"redis==5.2.1",
|
||||
"django-redis==5.4.0",
|
||||
"django-storages==1.14.4",
|
||||
"django-storages==1.14.5",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.1.5",
|
||||
"django==5.1.6",
|
||||
"djangorestframework==3.15.2",
|
||||
"drf_spectacular==0.28.0",
|
||||
"dockerflow==2024.4.2",
|
||||
@@ -48,18 +48,18 @@ dependencies = [
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"psycopg[binary]==3.2.4",
|
||||
"PyJWT==2.10.1",
|
||||
"joserfc==1.0.2",
|
||||
"joserfc==1.0.3",
|
||||
"requests==2.32.3",
|
||||
"sentry-sdk[django]==2.20.0",
|
||||
"whitenoise==6.8.2",
|
||||
"sentry-sdk[django]==2.21.0",
|
||||
"whitenoise==6.9.0",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Bug Tracker" = "https://github.com/numerique-gouv/people/issues/new"
|
||||
"Changelog" = "https://github.com/numerique-gouv/people/blob/main/CHANGELOG.md"
|
||||
"Homepage" = "https://github.com/numerique-gouv/people"
|
||||
"Repository" = "https://github.com/numerique-gouv/people"
|
||||
"Bug Tracker" = "https://github.com/suitenumerique/people/issues/new"
|
||||
"Changelog" = "https://github.com/suitenumerique/people/blob/main/CHANGELOG.md"
|
||||
"Homepage" = "https://github.com/suitenumerique/people"
|
||||
"Repository" = "https://github.com/suitenumerique/people"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
@@ -72,12 +72,12 @@ dev = [
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.4",
|
||||
"pytest-cov==6.0.0",
|
||||
"pytest-django==4.9.0",
|
||||
"pytest-django==4.10.0",
|
||||
"pytest==8.3.4",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.6.1",
|
||||
"responses==0.25.6",
|
||||
"ruff==0.9.4",
|
||||
"ruff==0.9.6",
|
||||
"types-requests==2.32.0.20241016",
|
||||
"freezegun==1.5.1",
|
||||
]
|
||||
@@ -140,6 +140,12 @@ python_files = [
|
||||
"test_*.py",
|
||||
"tests.py",
|
||||
]
|
||||
filterwarnings = [
|
||||
# This one can be removed when upgrading to Django 6.0
|
||||
'ignore:The FORMS_URLFIELD_ASSUME_HTTPS transitional setting is deprecated.',
|
||||
# This one can be removed after upgrading DRF to 3.16
|
||||
"ignore:Converter 'drf_format_suffix'"
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-desk",
|
||||
"version": "1.10.1",
|
||||
"version": "1.12.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -16,20 +16,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@hookform/resolvers": "3.10.0",
|
||||
"@hookform/resolvers": "4.0.0",
|
||||
"@openfun/cunningham-react": "3.0.0",
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"i18next": "24.2.2",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.5.0",
|
||||
"next": "15.1.6",
|
||||
"next": "15.1.7",
|
||||
"react": "*",
|
||||
"react-dom": "*",
|
||||
"react-hook-form": "7.54.2",
|
||||
"react-i18next": "15.4.0",
|
||||
"react-select": "5.10.0",
|
||||
"sass": "1.83.4",
|
||||
"styled-components": "6.1.14",
|
||||
"sass": "1.84.0",
|
||||
"styled-components": "6.1.15",
|
||||
"zod": "3.24.1",
|
||||
"zustand": "5.0.3"
|
||||
},
|
||||
@@ -53,7 +53,7 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.4.2",
|
||||
"prettier": "3.5.0",
|
||||
"stylelint": "16.14.1",
|
||||
"stylelint-config-standard": "37.0.0",
|
||||
"stylelint-prettier": "5.0.3",
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Modal: React.FC<ModalProps> = ({ children, ...props }) => {
|
||||
/**
|
||||
* @description used to prevent elements to be navigable by keyboard when only a DOM mutation causes the elements to be
|
||||
* in the document
|
||||
* @see https://github.com/numerique-gouv/people/pull/379
|
||||
* @see https://github.com/suitenumerique/people/pull/379
|
||||
*/
|
||||
export const usePreventFocusVisible = (elements: string[]) => {
|
||||
useEffect(() => {
|
||||
|
||||
@@ -62,7 +62,7 @@ export const LanguagePicker = () => {
|
||||
|
||||
/**
|
||||
* @description prevent select div to receive focus on keyboard navigation so the focus goes directly to inner button
|
||||
* @see https://github.com/numerique-gouv/people/pull/379
|
||||
* @see https://github.com/suitenumerique/people/pull/379
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!document) {
|
||||
|
||||
+1
@@ -99,6 +99,7 @@ describe('MailDomainAccessesPage', () => {
|
||||
status: 'enabled',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
support_email: 'support@example.com',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ describe('AccessAction', () => {
|
||||
status: 'enabled',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
support_email: 'support@example.com',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ describe('AccessesContent', () => {
|
||||
status: 'enabled',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
support_email: 'support@example.com',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ const mockMailDomain: MailDomain = {
|
||||
status: 'enabled',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
support_email: 'support@example.com',
|
||||
abilities: {
|
||||
manage_accesses: true,
|
||||
get: true,
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ describe('ModalDelete', () => {
|
||||
status: 'enabled',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
support_email: 'support@example.com',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { VariantType } from '@openfun/cunningham-react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { MailDomain } from '@/features/mail-domains/domains';
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import { MailDomainView } from '../components/MailDomainView';
|
||||
|
||||
const mockMailDomain: MailDomain = {
|
||||
id: '123e4567-e89b-12d3-a456-426614174000',
|
||||
name: 'example.com',
|
||||
status: 'action_required',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
slug: 'example-com',
|
||||
support_email: 'support@example.com',
|
||||
abilities: {
|
||||
delete: false,
|
||||
manage_accesses: true,
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
},
|
||||
action_required_details: {
|
||||
mx: 'Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr....',
|
||||
},
|
||||
expected_config: [
|
||||
{ target: '', type: 'mx', value: 'mx.ox.numerique.gouv.fr.' },
|
||||
{
|
||||
target: 'dimail._domainkey',
|
||||
type: 'txt',
|
||||
value:
|
||||
'v=DKIM1; h=sha256; k=rsa; p=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
},
|
||||
{ target: 'imap', type: 'cname', value: 'imap.ox.numerique.gouv.fr.' },
|
||||
{ target: 'smtp', type: 'cname', value: 'smtp.ox.numerique.gouv.fr.' },
|
||||
{
|
||||
target: '',
|
||||
type: 'txt',
|
||||
value: 'v=spf1 include:_spf.ox.numerique.gouv.fr -all',
|
||||
},
|
||||
{
|
||||
target: 'webmail',
|
||||
type: 'cname',
|
||||
value: 'webmail.ox.numerique.gouv.fr.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const toast = jest.fn();
|
||||
jest.mock('@openfun/cunningham-react', () => ({
|
||||
...jest.requireActual('@openfun/cunningham-react'),
|
||||
useToastProvider: () => ({
|
||||
toast,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('<MailDomainView />', () => {
|
||||
const apiUrl = `end:/mail-domains/${mockMailDomain.slug}/fetch/`;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it('display action required button and open modal with information when domain status is action_required', () => {
|
||||
render(<MailDomainView mailDomain={mockMailDomain} />, {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
// Check if action required button is displayed
|
||||
const actionButton = screen.getByText('Actions required');
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
|
||||
// Click the button and verify modal content
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
// Verify modal title and content
|
||||
expect(screen.getByText('Required actions on domain')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr./i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Verify DNS configuration section
|
||||
expect(
|
||||
screen.getByText(/DNS Configuration Required:/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/imap.ox.numerique.gouv.fr./i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/webmail.ox.numerique.gouv.fr./i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it('allows re-running domain check when clicking re-run button', async () => {
|
||||
// Mock the fetch call
|
||||
fetchMock.postOnce(apiUrl, {
|
||||
status: 200,
|
||||
body: mockMailDomain,
|
||||
});
|
||||
|
||||
render(
|
||||
<MailDomainView
|
||||
mailDomain={mockMailDomain}
|
||||
onMailDomainUpdate={jest.fn()}
|
||||
/>,
|
||||
{ wrapper: AppWrapper },
|
||||
);
|
||||
|
||||
// Check if action required button is displayed
|
||||
const actionButton = screen.getByText('Actions required');
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
|
||||
// Click the button and verify modal content
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
// Verify modal title and content
|
||||
expect(screen.getByText('Required actions on domain')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Find and click re-run button
|
||||
const reRunButton = screen.getByText('Re-run check');
|
||||
fireEvent.click(reRunButton);
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.called(apiUrl)).toBeTruthy();
|
||||
});
|
||||
expect(toast).toHaveBeenCalledWith(
|
||||
'Domain data fetched successfully',
|
||||
VariantType.SUCCESS,
|
||||
);
|
||||
});
|
||||
});
|
||||
+20
-30
@@ -19,6 +19,7 @@ describe('ModalAddMailDomain', () => {
|
||||
modalElement: screen.getByText('Add a mail domain'),
|
||||
formTag: screen.getByTitle('Mail domain addition form'),
|
||||
inputName: screen.getByLabelText(/Domain name/i),
|
||||
inputSupportEmail: screen.getByLabelText(/Support email address/i),
|
||||
buttonCancel: screen.getByRole('button', { name: /Cancel/i, hidden: true }),
|
||||
buttonSubmit: screen.getByRole('button', {
|
||||
name: /Add the domain/i,
|
||||
@@ -37,12 +38,19 @@ describe('ModalAddMailDomain', () => {
|
||||
it('renders all the elements', () => {
|
||||
render(<ModalAddMailDomain />, { wrapper: AppWrapper });
|
||||
|
||||
const { modalElement, formTag, inputName, buttonCancel, buttonSubmit } =
|
||||
getElements();
|
||||
const {
|
||||
modalElement,
|
||||
formTag,
|
||||
inputName,
|
||||
inputSupportEmail,
|
||||
buttonCancel,
|
||||
buttonSubmit,
|
||||
} = getElements();
|
||||
|
||||
expect(modalElement).toBeVisible();
|
||||
expect(formTag).toBeVisible();
|
||||
expect(inputName).toBeVisible();
|
||||
expect(inputSupportEmail).toBeVisible();
|
||||
expect(screen.getByText('Example: saint-laurent.fr')).toBeVisible();
|
||||
expect(buttonCancel).toBeVisible();
|
||||
expect(buttonSubmit).toBeVisible();
|
||||
@@ -104,9 +112,10 @@ describe('ModalAddMailDomain', () => {
|
||||
|
||||
render(<ModalAddMailDomain />, { wrapper: AppWrapper });
|
||||
|
||||
const { inputName, buttonSubmit } = getElements();
|
||||
const { inputName, inputSupportEmail, buttonSubmit } = getElements();
|
||||
|
||||
await user.type(inputName, 'domain.fr');
|
||||
await user.type(inputSupportEmail, 'support@domain.fr');
|
||||
|
||||
await user.click(buttonSubmit);
|
||||
|
||||
@@ -114,6 +123,7 @@ describe('ModalAddMailDomain', () => {
|
||||
expect(fetchMock.lastOptions()).toEqual({
|
||||
body: JSON.stringify({
|
||||
name: 'domain.fr',
|
||||
support_email: 'support@domain.fr',
|
||||
}),
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -123,29 +133,6 @@ describe('ModalAddMailDomain', () => {
|
||||
expect(mockPush).toHaveBeenCalledWith(`/mail-domains/domainfr`);
|
||||
});
|
||||
|
||||
it('submits the form on key enter press', async () => {
|
||||
fetchMock.mock(`end:mail-domains/`, 201);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<ModalAddMailDomain />, { wrapper: AppWrapper });
|
||||
|
||||
const { inputName } = getElements();
|
||||
|
||||
await user.type(inputName, 'domain.fr');
|
||||
await user.type(inputName, '{enter}');
|
||||
|
||||
expect(fetchMock.lastUrl()).toContain('/mail-domains/');
|
||||
expect(fetchMock.lastOptions()).toEqual({
|
||||
body: JSON.stringify({
|
||||
name: 'domain.fr',
|
||||
}),
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
});
|
||||
});
|
||||
|
||||
it('displays right error message error when maildomain name is already used', async () => {
|
||||
fetchMock.mock(`end:mail-domains/`, {
|
||||
status: 400,
|
||||
@@ -158,10 +145,10 @@ describe('ModalAddMailDomain', () => {
|
||||
|
||||
render(<ModalAddMailDomain />, { wrapper: AppWrapper });
|
||||
|
||||
const { inputName, buttonSubmit } = getElements();
|
||||
const { inputName, inputSupportEmail, buttonSubmit } = getElements();
|
||||
|
||||
await user.type(inputName, 'domain.fr');
|
||||
|
||||
await user.type(inputSupportEmail, 'support@domain.fr');
|
||||
await user.click(buttonSubmit);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -175,6 +162,7 @@ describe('ModalAddMailDomain', () => {
|
||||
expect(inputName).toHaveFocus();
|
||||
|
||||
await user.type(inputName, 'domain2.fr');
|
||||
|
||||
expect(buttonSubmit).toBeEnabled();
|
||||
});
|
||||
|
||||
@@ -190,9 +178,10 @@ describe('ModalAddMailDomain', () => {
|
||||
|
||||
render(<ModalAddMailDomain />, { wrapper: AppWrapper });
|
||||
|
||||
const { inputName, buttonSubmit } = getElements();
|
||||
const { inputName, inputSupportEmail, buttonSubmit } = getElements();
|
||||
|
||||
await user.type(inputName, 'domainfr');
|
||||
await user.type(inputSupportEmail, 'support@domain.fr');
|
||||
|
||||
await user.click(buttonSubmit);
|
||||
|
||||
@@ -220,9 +209,10 @@ describe('ModalAddMailDomain', () => {
|
||||
|
||||
render(<ModalAddMailDomain />, { wrapper: AppWrapper });
|
||||
|
||||
const { inputName, buttonSubmit } = getElements();
|
||||
const { inputName, inputSupportEmail, buttonSubmit } = getElements();
|
||||
|
||||
await user.type(inputName, 'domain.fr');
|
||||
await user.type(inputSupportEmail, 'support@domain.fr');
|
||||
|
||||
await user.click(buttonSubmit);
|
||||
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { fetchMailDomain } from '@/features/mail-domains/domains/api/useFetchMailDomain';
|
||||
import { MailDomain } from '@/features/mail-domains/domains/types';
|
||||
|
||||
const mockMailDomain: MailDomain = {
|
||||
id: '123e4567-e89b-12d3-a456-426614174000',
|
||||
name: 'example.com',
|
||||
status: 'enabled',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
slug: 'example-com',
|
||||
support_email: 'support@example.com',
|
||||
abilities: {
|
||||
delete: false,
|
||||
manage_accesses: true,
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
},
|
||||
action_required_details: {
|
||||
mx: 'Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr.',
|
||||
},
|
||||
expected_config: [
|
||||
{ target: '', type: 'mx', value: 'mx.ox.numerique.gouv.fr.' },
|
||||
{
|
||||
target: 'dimail._domainkey',
|
||||
type: 'txt',
|
||||
value: 'v=DKIM1; h=sha256; k=rsa; p=X...X',
|
||||
},
|
||||
{ target: 'imap', type: 'cname', value: 'imap.ox.numerique.gouv.fr.' },
|
||||
{ target: 'smtp', type: 'cname', value: 'smtp.ox.numerique.gouv.fr.' },
|
||||
{
|
||||
target: '',
|
||||
type: 'txt',
|
||||
value: 'v=spf1 include:_spf.ox.numerique.gouv.fr -all',
|
||||
},
|
||||
{
|
||||
target: 'webmail',
|
||||
type: 'cname',
|
||||
value: 'webmail.ox.numerique.gouv.fr.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('fetchMailDomain', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it('fetch the domain successfully', async () => {
|
||||
fetchMock.postOnce('end:/mail-domains/example-slug/fetch/', {
|
||||
status: 200,
|
||||
body: mockMailDomain,
|
||||
});
|
||||
|
||||
const result = await fetchMailDomain('example-slug');
|
||||
|
||||
expect(result).toEqual(mockMailDomain);
|
||||
expect(fetchMock.calls()).toHaveLength(1);
|
||||
expect(fetchMock.lastUrl()).toContain('/mail-domains/example-slug/fetch/');
|
||||
});
|
||||
|
||||
it('throw an error when the domain is not found', async () => {
|
||||
fetchMock.postOnce('end:/mail-domains/example-slug/fetch/', {
|
||||
status: 404,
|
||||
body: { cause: ['Domain not found'] },
|
||||
});
|
||||
|
||||
await expect(fetchMailDomain('example-slug')).rejects.toThrow(APIError);
|
||||
expect(fetchMock.calls()).toHaveLength(1);
|
||||
expect(fetchMock.lastUrl()).toContain('/mail-domains/example-slug/fetch/');
|
||||
});
|
||||
});
|
||||
@@ -8,16 +8,16 @@ import { KEY_LIST_MAIL_DOMAIN } from './useMailDomains';
|
||||
|
||||
export interface AddMailDomainParams {
|
||||
name: string;
|
||||
supportEmail: string;
|
||||
}
|
||||
|
||||
export const addMailDomain = async (
|
||||
name: AddMailDomainParams['name'],
|
||||
): Promise<MailDomain> => {
|
||||
export const addMailDomain = async ({
|
||||
name,
|
||||
supportEmail,
|
||||
}: AddMailDomainParams): Promise<MailDomain> => {
|
||||
const response = await fetchAPI(`mail-domains/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
}),
|
||||
body: JSON.stringify({ name, support_email: supportEmail }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -38,7 +38,7 @@ export const useAddMailDomain = ({
|
||||
onError: (error: APIError) => void;
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<MailDomain, APIError, string>({
|
||||
return useMutation<MailDomain, APIError, AddMailDomainParams>({
|
||||
mutationFn: addMailDomain,
|
||||
onSuccess: (data) => {
|
||||
void queryClient.invalidateQueries({
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
import { MailDomain } from '../types';
|
||||
|
||||
export const fetchMailDomain = async (slug: string): Promise<MailDomain> => {
|
||||
const response = await fetchAPI(`mail-domains/${slug}/fetch/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to fetch domain from Dimail',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<MailDomain>;
|
||||
};
|
||||
|
||||
export const useFetchFromDimail = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess: (data: MailDomain) => void;
|
||||
onError: (error: APIError) => void;
|
||||
}) => {
|
||||
return useMutation<MailDomain, APIError, string>({
|
||||
mutationFn: fetchMailDomain,
|
||||
onSuccess: (data) => {
|
||||
onSuccess(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
};
|
||||
+182
-20
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalSize,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -9,11 +16,17 @@ import MailDomainsLogo from '@/features/mail-domains/assets/mail-domains-logo.sv
|
||||
import { MailDomain, Role } from '@/features/mail-domains/domains';
|
||||
import { MailDomainsContent } from '@/features/mail-domains/mailboxes';
|
||||
|
||||
import { useFetchFromDimail } from '../api/useFetchMailDomain';
|
||||
|
||||
type Props = {
|
||||
mailDomain: MailDomain;
|
||||
onMailDomainUpdate?: (updatedDomain: MailDomain) => void;
|
||||
};
|
||||
export const MailDomainView = ({ mailDomain }: Props) => {
|
||||
|
||||
export const MailDomainView = ({ mailDomain, onMailDomainUpdate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToastProvider();
|
||||
const [showModal, setShowModal] = React.useState(false);
|
||||
const currentRole = mailDomain.abilities.delete
|
||||
? Role.OWNER
|
||||
: mailDomain.abilities.manage_accesses
|
||||
@@ -41,28 +54,177 @@ export const MailDomainView = ({ mailDomain }: Props) => {
|
||||
];
|
||||
}, [t, currentRole, mailDomain]);
|
||||
|
||||
const handleShowModal = () => {
|
||||
setShowModal(true);
|
||||
};
|
||||
const copyToClipboard = async (text: string) => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast(t('copy done'), VariantType.SUCCESS);
|
||||
};
|
||||
|
||||
const { mutate: fetchMailDomain } = useFetchFromDimail({
|
||||
onSuccess: (data: MailDomain) => {
|
||||
console.info('fetchMailDomain success', data);
|
||||
setShowModal(false);
|
||||
toast(t('Domain data fetched successfully'), VariantType.SUCCESS);
|
||||
onMailDomainUpdate?.(data);
|
||||
},
|
||||
onError: () => {
|
||||
console.error('fetchMailDomain error');
|
||||
toast(t('Failed to fetch domain data'), VariantType.ERROR);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Box $padding="big">
|
||||
<Box
|
||||
$width="100%"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="2.25rem"
|
||||
$justify="center"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="center"
|
||||
$margin={{ bottom: 'big' }}
|
||||
$gap="0.5rem"
|
||||
<>
|
||||
{showModal && (
|
||||
<Modal
|
||||
isOpen
|
||||
size={ModalSize.EXTRA_LARGE}
|
||||
onClose={() => setShowModal(false)}
|
||||
title={t('Required actions on domain')}
|
||||
>
|
||||
<MailDomainsLogo aria-hidden="true" />
|
||||
<Text $margin="none" as="h3" $size="h3">
|
||||
{mailDomain?.name}
|
||||
</Text>
|
||||
<p>
|
||||
{t(
|
||||
'The domain is currently in action required status. Please take the necessary actions to resolve those following issues.',
|
||||
)}
|
||||
</p>
|
||||
<h3>{t('Actions required detail')}</h3>
|
||||
|
||||
<pre>
|
||||
{mailDomain.action_required_details &&
|
||||
Object.entries(mailDomain.action_required_details).map(
|
||||
([check, value], index) => (
|
||||
<ul key={`action-required-list-${index}`}>
|
||||
<li key={`action-required-${index}`}>
|
||||
<b>{check}</b>: {value}
|
||||
</li>
|
||||
</ul>
|
||||
),
|
||||
)}
|
||||
</pre>
|
||||
{mailDomain.expected_config && (
|
||||
<Box $margin={{ bottom: 'medium' }}>
|
||||
<h3>{t('DNS Configuration Required:')}</h3>
|
||||
<pre>
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
>
|
||||
{t('Add the following DNS values:')}
|
||||
<ul>
|
||||
{mailDomain.expected_config.map((item, index) => (
|
||||
<li
|
||||
key={`dns-record-${index}`}
|
||||
style={{ marginBottom: '10px' }}
|
||||
>
|
||||
{item.target && (
|
||||
<>
|
||||
<b>{item.target.toUpperCase()}</b> -{' '}
|
||||
</>
|
||||
)}
|
||||
<b>{item.type.toUpperCase()}</b> {t('with value:')}{' '}
|
||||
<span style={{ backgroundColor: '#d4e5f5' }}>
|
||||
{item.value}
|
||||
</span>
|
||||
<button
|
||||
style={{
|
||||
padding: '2px 5px',
|
||||
marginLeft: '10px',
|
||||
backgroundColor: '#cccccc',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
onClick={() => {
|
||||
void copyToClipboard(item.value);
|
||||
}}
|
||||
>
|
||||
{t('Copy')}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</pre>
|
||||
</Box>
|
||||
)}
|
||||
<pre>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
void fetchMailDomain(mailDomain.slug);
|
||||
}}
|
||||
>
|
||||
{t('Re-run check')}
|
||||
</Button>
|
||||
</div>
|
||||
</pre>
|
||||
</Modal>
|
||||
)}
|
||||
<Box $padding="big">
|
||||
<Box
|
||||
$width="100%"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="2.25rem"
|
||||
$justify="center"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="center"
|
||||
$margin={{ bottom: 'big' }}
|
||||
$gap="0.5rem"
|
||||
>
|
||||
<MailDomainsLogo aria-hidden="true" />
|
||||
<Text $margin="none" as="h3" $size="h3">
|
||||
{mailDomain?.name}
|
||||
</Text>
|
||||
{/* TODO: remove when pending status will be removed */}
|
||||
{mailDomain?.status === 'pending' && (
|
||||
<button
|
||||
onClick={handleShowModal}
|
||||
style={{
|
||||
padding: '5px 10px',
|
||||
marginLeft: '10px',
|
||||
backgroundColor: '#cccccc',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
data-modal="mail-domain-status"
|
||||
>
|
||||
{t('Pending')}
|
||||
</button>
|
||||
)}
|
||||
{mailDomain?.status === 'action_required' && (
|
||||
<button
|
||||
onClick={handleShowModal}
|
||||
style={{
|
||||
padding: '5px 10px',
|
||||
marginLeft: '10px',
|
||||
backgroundColor: '#f37802',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
data-modal="mail-domain-status"
|
||||
>
|
||||
{t('Actions required')}
|
||||
</button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<CustomTabs tabs={tabs} />
|
||||
</Box>
|
||||
<CustomTabs tabs={tabs} />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+28
-4
@@ -6,6 +6,7 @@ import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { parseAPIError } from '@/api/parseAPIError';
|
||||
import { Box, Text, TextErrors } from '@/components';
|
||||
import { Modal } from '@/components/Modal';
|
||||
@@ -23,12 +24,14 @@ export const ModalAddMailDomain = () => {
|
||||
|
||||
const addMailDomainValidationSchema = z.object({
|
||||
name: z.string().min(1, t('Example: saint-laurent.fr')),
|
||||
supportEmail: z.string().email(t('Please enter a valid email address')),
|
||||
});
|
||||
|
||||
const methods = useForm<{ name: string }>({
|
||||
const methods = useForm<{ name: string; supportEmail: string }>({
|
||||
delayError: 0,
|
||||
defaultValues: {
|
||||
name: '',
|
||||
supportEmail: '',
|
||||
},
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
@@ -39,7 +42,7 @@ export const ModalAddMailDomain = () => {
|
||||
onSuccess: (mailDomain) => {
|
||||
router.push(`/mail-domains/${mailDomain.slug}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (error: APIError) => {
|
||||
const unhandledCauses = parseAPIError({
|
||||
error,
|
||||
errorParams: [
|
||||
@@ -87,8 +90,8 @@ export const ModalAddMailDomain = () => {
|
||||
const onSubmitCallback = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
void methods.handleSubmit(({ name }) => {
|
||||
void addMailDomain(name);
|
||||
void methods.handleSubmit(({ name, supportEmail }) => {
|
||||
void addMailDomain({ name, supportEmail });
|
||||
})();
|
||||
};
|
||||
|
||||
@@ -167,6 +170,27 @@ export const ModalAddMailDomain = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Box $margin={{ vertical: '10px' }}>
|
||||
<Controller
|
||||
control={methods.control}
|
||||
name="supportEmail"
|
||||
render={({ fieldState }) => (
|
||||
<Input
|
||||
aria-invalid={!!fieldState.error}
|
||||
aria-required
|
||||
required
|
||||
label={t('Support email address')}
|
||||
state={fieldState.error ? 'error' : 'default'}
|
||||
text={
|
||||
fieldState?.error?.message
|
||||
? fieldState.error.message
|
||||
: t('E.g. : support@example.fr')
|
||||
}
|
||||
{...methods.register('supportEmail')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</form>
|
||||
|
||||
{isPending && (
|
||||
|
||||
+2
@@ -31,6 +31,8 @@ export const PanelMailDomains = ({ mailDomain }: MailDomainProps) => {
|
||||
return t('[disabled]');
|
||||
case 'failed':
|
||||
return t('[failed]');
|
||||
case 'action_required':
|
||||
return t('[action required]');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ export interface MailDomain {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
slug: string;
|
||||
status: 'pending' | 'enabled' | 'failed' | 'disabled';
|
||||
status: 'pending' | 'enabled' | 'failed' | 'disabled' | 'action_required';
|
||||
support_email: string;
|
||||
action_required_details?: Record<string, string>;
|
||||
abilities: {
|
||||
get: boolean;
|
||||
patch: boolean;
|
||||
@@ -15,6 +17,11 @@ export interface MailDomain {
|
||||
delete: boolean;
|
||||
manage_accesses: boolean;
|
||||
};
|
||||
expected_config?: Array<{
|
||||
target: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export enum Role {
|
||||
|
||||
+2
@@ -14,6 +14,7 @@ const mockMailDomain: MailDomain = {
|
||||
name: 'example.com',
|
||||
slug: 'example-com',
|
||||
status: 'enabled',
|
||||
support_email: 'support@example.com',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
@@ -31,6 +32,7 @@ const mockMailDomainAsViewer: MailDomain = {
|
||||
name: 'example.com',
|
||||
slug: 'example-com',
|
||||
status: 'enabled',
|
||||
support_email: 'support@example.com',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: false,
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ const mockMailDomain: MailDomain = {
|
||||
status: 'enabled',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
support_email: 'support@example.com',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
{
|
||||
"de": {
|
||||
"translation": {
|
||||
"Accessibility: non-compliant": "Barrierefreiheit: nicht kompatibel",
|
||||
"Cancel": "",
|
||||
"Language": "Sprache",
|
||||
"Legal Notice": "Impressum",
|
||||
"Logout": "Abmelden",
|
||||
"No Username": "Kein Benutzername",
|
||||
"Personal data and cookies": "Personenbezogene Daten und Cookies"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"translation": {
|
||||
"{{count}} member_many": "{{count}} members",
|
||||
@@ -14,22 +25,28 @@
|
||||
"Accesses list card": "Carte de la liste des accès",
|
||||
"Accessibility statement": "Déclaration d'accessibilité",
|
||||
"Accessibility: non-compliant": "Accessibilité : non conforme",
|
||||
"Actions required": "Actions requises",
|
||||
"Actions required detail": "Détail des actions requises",
|
||||
"Add a mail domain": "Ajouter un nom de domaine",
|
||||
"Add a member": "Ajouter un membre",
|
||||
"Add a team": "Ajouter un groupe",
|
||||
"Add members to the team": "Ajouter des membres à l'équipe",
|
||||
"Add the domain": "Ajouter le domaine",
|
||||
"Add the following DNS values:": "Ajouter les valeurs DNS suivantes :",
|
||||
"Add to group": "Ajouter au groupe",
|
||||
"Address: National Agency for Territorial Cohesion - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07 Paris": "Adresse : Agence Nationale de la Cohésion des Territoires - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07",
|
||||
"Administration": "Administration",
|
||||
"Administrator": "Administrateur",
|
||||
"All fields are mandatory.": "Tous les champs sont obligatoires.",
|
||||
"Are you sure you want to delete {{teamName}} team?": "Êtes-vous sûr de vouloir supprimer le groupe {{teamName}}?",
|
||||
"Are you sure you want to disable this mailbox? This action results in the deletion of the calendar, address book, etc.": "Êtes-vous sûr de vouloir désactiver cette boîte mail ? Cette action entraîne la suppression du calendrier, du carnet d'adresses, etc.",
|
||||
"Are you sure you want to remove this access from the {{domain}} domain?": "Voulez-vous vraiment retirer cet accès du domaine {{domain}} ?",
|
||||
"Are you sure you want to remove this member from the {{team}} group?": "Voulez-vous vraiment retirer ce membre du groupe {{team}} ?",
|
||||
"Back to home page": "Retour à l'accueil",
|
||||
"Cancel": "Annuler",
|
||||
"Choose a role": "Choisissez un rôle",
|
||||
"Click on mailbox to view details": "Cliquez sur la boîte mail pour voir les détails",
|
||||
"Click on team to view details": "Cliquez sur l'équipe pour voir les détails",
|
||||
"Close the mail domains panel": "Fermer le panneau des domaines de messagerie",
|
||||
"Close the modal": "Fermer la modale",
|
||||
"Close the teams panel": "Fermer le panneau des groupes",
|
||||
@@ -39,6 +56,7 @@
|
||||
"Content modal to delete the team": "Contenu modal pour supprimer le groupe",
|
||||
"Content modal to update the team": "Contenu modal pour mettre à jour le groupe",
|
||||
"Cookies placed": "Cookies déposés",
|
||||
"Copy": "Copier",
|
||||
"Copyright": "Copyright",
|
||||
"Create a mailbox": "Créer une boîte mail",
|
||||
"Create a mailbox in {{name}} domain": "Créer une boîte mail dans le domaine {{name}}",
|
||||
@@ -49,20 +67,28 @@
|
||||
"Create the team": "Créer le groupe",
|
||||
"Create your first team by clicking on the \"Create a new team\" button.": "Créez votre premier groupe en cliquant sur le bouton \"Créer un nouveau groupe\".",
|
||||
"Created at": "Créé le",
|
||||
"DNS Configuration Required:": "Configuration DNS requise :",
|
||||
"Declaration established on June 25, 2024.": "Établie le 25 juin 2024.",
|
||||
"Defender of Rights - Free response - 71120 75342 Paris CEDEX 07": "Défenseur des droits\nLibre réponse 71120 75342 Paris CEDEX 07",
|
||||
"Delete the team": "Supprimer le groupe",
|
||||
"Deleting the {{teamName}} team": "Suppression du groupe {{teamName}}",
|
||||
"Disable": "Désactiver",
|
||||
"Disable mailbox": "Désactiver la boîte mail",
|
||||
"Domain data fetched successfully": "Les données de domaine ont été récupérées avec succès",
|
||||
"Domain name": "Nom de domaine",
|
||||
"E-mail:": "E-mail:",
|
||||
"E.g. : jean.dupont@mail.fr": "Ex. : jean.dupont@mail.fr",
|
||||
"E.g. : support@example.fr": "Par exemple : support@example.fr",
|
||||
"Email address prefix": "Préfixe de l'adresse mail",
|
||||
"Emails": "Emails",
|
||||
"Empty team icon": "Icône équipe vide",
|
||||
"Enable mailbox": "Activer la boîte mail",
|
||||
"Enter the new name of the selected team": "Entrez le nouveau nom du groupe sélectionné",
|
||||
"Example: saint-laurent.fr": "Exemple : saint-laurent.fr",
|
||||
"Failed to add {{name}} in the team": "Impossible d'ajouter {{name}} au groupe",
|
||||
"Failed to create the invitation for {{email}}": "Impossible de créer l'invitation pour {{email}}",
|
||||
"Failed to fetch domain data": "Impossible de récupérer les données du domaine",
|
||||
"Failed to update mailbox status": "Impossible de mettre à jour le statut de la boîte mail",
|
||||
"Filter member list": "Filtrer la liste des membres",
|
||||
"Find a member to add to the team": "Trouver un membre à ajouter au groupe",
|
||||
"First name": "Prénom",
|
||||
@@ -121,13 +147,16 @@
|
||||
"Open the teams panel": "Ouvrir le panneau des groupes",
|
||||
"Ouch!": "Aïe !",
|
||||
"Owner": "Propriétaire",
|
||||
"Pending": "En attente",
|
||||
"Personal data and cookies": "Données personnelles et cookies",
|
||||
"Please enter a valid email address": "Merci de saisir une adresse e-mail valide",
|
||||
"Please enter a valid email address.\nE.g. : jean.dupont@mail.fr": "Veuillez entrer une adresse e-mail valide.\nEx. : jean.dupont@mail.fr",
|
||||
"Please enter your first name": "Veuillez saisir votre prénom",
|
||||
"Please enter your last name": "Veuillez saisir votre nom",
|
||||
"Publication Director": "Directeur de la publication",
|
||||
"Publisher": "Éditeur",
|
||||
"Radio buttons to update the roles": "Boutons radio pour mettre à jour les rôles",
|
||||
"Re-run check": "Ré-exécuter la vérification",
|
||||
"Remedy": "Voie de recours",
|
||||
"Remove from domain": "Retirer du domaine",
|
||||
"Remove from group": "Retirer du groupe",
|
||||
@@ -135,6 +164,7 @@
|
||||
"Remove from the group": "Retirer du groupe",
|
||||
"Remove this access from the domain": "Retirer cet accès du domaine",
|
||||
"Remove this member from the group": "Retirer le membre du groupe",
|
||||
"Required actions on domain": "Actions requises sur le domaine",
|
||||
"Roles": "Rôles",
|
||||
"Régie": "Régie",
|
||||
"Search new members (name or email)": "Rechercher de nouveaux membres (nom ou email)",
|
||||
@@ -147,12 +177,15 @@
|
||||
"Sort the domain names by creation date descendent": "Trier les documents par date de création descendante",
|
||||
"Sort the teams by creation date ascendent": "Trier les groupes par date de création ascendante",
|
||||
"Sort the teams by creation date descendent": "Trier les groupes par date de création descendante",
|
||||
"Status": "Statut",
|
||||
"Stéphanie Schaer: Interministerial Digital Director (DINUM).": "Stéphanie Schaer: Directrice numérique interministériel (DINUM).",
|
||||
"Support email address": "Adresse e-mail du support",
|
||||
"Team icon": "Icône équipe",
|
||||
"Team name": "Nom du groupe",
|
||||
"Teams": "Équipes",
|
||||
"The National Agency for Territorial Cohesion undertakes to make its\n service accessible, in accordance with article 47 of law no. 2005-102\n of February 11, 2005.": "L'Agence Nationale de la Cohésion des Territoires s’engage à rendre son service accessible, conformément à l’article 47 de la loi n° 2005-102 du 11 février 2005.",
|
||||
"The access has been removed from the domain": "L'accès a été supprimé du domaine",
|
||||
"The domain is currently in action required status. Please take the necessary actions to resolve those following issues.": "Le domaine est actuellement en statut actions requises. Veuillez faire le nécessaire pour résoudre les problèmes suivants.",
|
||||
"The domain name encounters an error. Please contact our support team to solve the problem:": "Le nom de domaine rencontre une erreur. Veuillez contacter notre support pour résoudre le problème :",
|
||||
"The mail domain secret is misconfigured. Please, contact our support team to solve the issue: suiteterritoriale@anct.gouv.fr": "Le secret du domaine de messagerie est mal configuré. Veuillez contacter notre support pour résoudre le problème : suiteterritoriale@anct.gouv.fr",
|
||||
"The member has been removed from the team": "Le membre a été supprimé de votre groupe",
|
||||
@@ -188,15 +221,17 @@
|
||||
"You cannot remove other owner.": "Vous ne pouvez pas supprimer un autre propriétaire.",
|
||||
"You cannot update the role of other owner.": "Vous ne pouvez pas mettre à jour les rôles d'autre propriétaire.",
|
||||
"You must have minimum 1 character": "Vous devez entrer au moins 1 caractère",
|
||||
"Your domain name is being validated. You will not be able to create mailboxes until your domain name has been validated by our team.": "Votre nom de domaine est en cours de validation. Vous ne pourrez créer de boîtes mail que lorsque votre nom de domaine sera validé par notre équipe.",
|
||||
"Your request cannot be processed because the server is experiencing an error. If the problem persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr": "Votre demande ne peut pas être traitée car le serveur rencontre une erreur. Si le problème persiste, veuillez contacter notre support pour résoudre le problème : suiteterritoriale@anct.gouv.fr",
|
||||
"[action required]": "[action requise]",
|
||||
"[disabled]": "[désactivé]",
|
||||
"[enabled]": "[actif]",
|
||||
"[failed]": "[erroné]",
|
||||
"[failed]": "[en erreur]",
|
||||
"[pending]": "[en attente]",
|
||||
"accessibility-contact-defenseurdesdroits": "Contacter le délégué du<1>Défenseur des droits dans votre région</1>",
|
||||
"accessibility-form-defenseurdesdroits": "Écrire un message au<1>Défenseur des droits</1>",
|
||||
"copy done": "copié",
|
||||
"mail domains list loading": "chargement de la liste des domaines de messagerie",
|
||||
"with value:": "avec la valeur:",
|
||||
"{{count}} member_many": "{{count}} membres",
|
||||
"{{count}} member_one": "{{count}} membre",
|
||||
"{{count}} member_other": "{{count}} membres",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter as useNavigate } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { ReactElement } from 'react';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { TextErrors } from '@/components/TextErrors';
|
||||
import {
|
||||
MailDomain,
|
||||
MailDomainsLayout,
|
||||
useMailDomain,
|
||||
} from '@/features/mail-domains/domains';
|
||||
@@ -14,13 +15,15 @@ import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const MailboxesPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const [currentMailDomain, setCurrentMailDomain] = useState<MailDomain | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
if (router?.query?.slug && typeof router.query.slug !== 'string') {
|
||||
throw new Error('Invalid mail domain slug');
|
||||
}
|
||||
|
||||
const { slug } = router.query;
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
@@ -30,6 +33,13 @@ const MailboxesPage: NextPageWithLayout = () => {
|
||||
isLoading,
|
||||
} = useMailDomain({ slug: String(slug) });
|
||||
|
||||
// Update currentMailDomain when mailDomain changes
|
||||
React.useEffect(() => {
|
||||
if (mailDomain) {
|
||||
setCurrentMailDomain(mailDomain);
|
||||
}
|
||||
}, [mailDomain]);
|
||||
|
||||
if (error?.status === 404) {
|
||||
navigate.replace(`/404`);
|
||||
return null;
|
||||
@@ -47,11 +57,16 @@ const MailboxesPage: NextPageWithLayout = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!mailDomain) {
|
||||
if (!currentMailDomain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <MailDomainView mailDomain={mailDomain} />;
|
||||
return (
|
||||
<MailDomainView
|
||||
mailDomain={currentMailDomain}
|
||||
onMailDomainUpdate={setCurrentMailDomain}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
MailboxesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'marie');
|
||||
});
|
||||
|
||||
test.describe('When a commune, domain is created on first login via ProConnect', () => {
|
||||
test('it checks the domain has been created and is operational', async ({
|
||||
page,
|
||||
}) => {
|
||||
const header = page.locator('header').first();
|
||||
await expect(header.getByAltText('Marianne Logo')).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator('menu')
|
||||
.first()
|
||||
.getByLabel(`Mail Domains button`)
|
||||
.click();
|
||||
await expect(page).toHaveURL(/mail-domains\//);
|
||||
await expect(
|
||||
page.getByLabel('Mail domains panel', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('merlaut.collectivite.fr')).toHaveCount(1);
|
||||
await expect(page.getByText('No domains exist.')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ const getElements = (page: Page) => {
|
||||
});
|
||||
const form = page.locator('form');
|
||||
const inputName = form.getByLabel('Domain name');
|
||||
const inputSupportEmail = form.getByLabel('Support email address');
|
||||
const buttonSubmit = page.getByRole('button', {
|
||||
name: 'Add the domain',
|
||||
});
|
||||
@@ -22,6 +23,7 @@ const getElements = (page: Page) => {
|
||||
linkIndexPageAddDomain,
|
||||
form,
|
||||
inputName,
|
||||
inputSupportEmail,
|
||||
buttonCancel,
|
||||
buttonSubmit,
|
||||
};
|
||||
@@ -50,6 +52,7 @@ test.describe('Add Mail Domains', () => {
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(inputName).toBeVisible();
|
||||
await expect(inputName).toBeVisible();
|
||||
|
||||
await expect(page.getByText('Example: saint-laurent.fr')).toBeVisible();
|
||||
@@ -82,12 +85,17 @@ test.describe('Add Mail Domains', () => {
|
||||
test('checks form invalid status', async ({ page }) => {
|
||||
await page.goto('/mail-domains/');
|
||||
|
||||
const { linkIndexPageAddDomain, inputName, buttonSubmit } =
|
||||
getElements(page);
|
||||
const {
|
||||
linkIndexPageAddDomain,
|
||||
inputName,
|
||||
inputSupportEmail,
|
||||
buttonSubmit,
|
||||
} = getElements(page);
|
||||
|
||||
await linkIndexPageAddDomain.click();
|
||||
|
||||
await expect(inputName).toBeVisible();
|
||||
await expect(inputSupportEmail).toBeVisible();
|
||||
await expect(page.getByText('Example: saint-laurent.fr')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
@@ -111,16 +119,23 @@ test.describe('Add Mail Domains', () => {
|
||||
browserName,
|
||||
}) => {
|
||||
const mailDomainName = randomName('versailles.fr', browserName, 1)[0];
|
||||
const mailDomainSupportMail = 'support@'.concat(mailDomainName);
|
||||
const mailDomainSlug = mailDomainName.replace('.', '');
|
||||
|
||||
await page.goto('/mail-domains/');
|
||||
|
||||
const { linkIndexPageAddDomain, inputName, buttonSubmit } =
|
||||
getElements(page);
|
||||
const {
|
||||
linkIndexPageAddDomain,
|
||||
inputName,
|
||||
inputSupportEmail,
|
||||
buttonSubmit,
|
||||
} = getElements(page);
|
||||
|
||||
await linkIndexPageAddDomain.click();
|
||||
|
||||
await inputName.fill(mailDomainName);
|
||||
await inputSupportEmail.fill(mailDomainSupportMail);
|
||||
|
||||
await buttonSubmit.click();
|
||||
|
||||
await expect(page).toHaveURL(`/mail-domains/${mailDomainSlug}/`);
|
||||
|
||||
@@ -14,7 +14,7 @@ test.describe('OIDC interop with SIRET', () => {
|
||||
);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
expect(await response.json()).toMatchObject({
|
||||
organization: { registration_id_list: ['21580304000017'] },
|
||||
organization: { registration_id_list: ['21510339100011'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,6 @@ test.describe('When a commune, display commune name below user name', () => {
|
||||
name: 'Marie Delamairie',
|
||||
});
|
||||
|
||||
await expect(logout.getByText('Varzy')).toBeVisible();
|
||||
await expect(logout.getByText('Merlaut')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "1.10.1",
|
||||
"version": "1.12.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
|
||||
@@ -44,7 +44,7 @@ export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'], locale: 'en-US' },
|
||||
use: { ...devices['Desktop Chrome'], locale: 'en-US', channel: 'chrome' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "people",
|
||||
"version": "1.10.1",
|
||||
"version": "1.12.0",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -24,7 +24,7 @@
|
||||
"i18n:test": "yarn I18N run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "22.13.0",
|
||||
"@types/node": "22.13.1",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "eslint-config-people",
|
||||
"version": "1.10.1",
|
||||
"version": "1.12.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@tanstack/eslint-plugin-query": "5.66.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.22.0",
|
||||
"@typescript-eslint/parser": "8.22.0",
|
||||
"@next/eslint-plugin-next": "15.1.7",
|
||||
"@tanstack/eslint-plugin-query": "5.66.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.24.0",
|
||||
"@typescript-eslint/parser": "8.24.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jest": "28.11.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "1.10.1",
|
||||
"version": "1.12.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:desk",
|
||||
|
||||
+148
-143
@@ -1413,10 +1413,10 @@
|
||||
use-deep-compare-effect "^1.8.1"
|
||||
uuid "^8.3.2"
|
||||
|
||||
"@hookform/resolvers@3.10.0":
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.10.0.tgz#7bfd18113daca4e57e27e1205b7d5a2d371aa59a"
|
||||
integrity sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==
|
||||
"@hookform/resolvers@4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-4.0.0.tgz#730cb3a32bc236059264ec9fb7563f4eb3f4fb9d"
|
||||
integrity sha512-93ZueVlTaeMF0pRbrLbcnzrxeb2mGA/xyO3RgfrsKs5OCtcfjoWcdjBJm+N7096Jfg/JYlGPjuyQCgqVEodSTg==
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.14":
|
||||
version "0.11.14"
|
||||
@@ -1834,57 +1834,57 @@
|
||||
dependencies:
|
||||
buffer "^6.0.3"
|
||||
|
||||
"@next/env@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.6.tgz#2fa863d8c568a56b1c8328a86e621b8bdd4f2a20"
|
||||
integrity sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==
|
||||
"@next/env@15.1.7":
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.7.tgz#14e2678f893aec50ff2dcb7a6665092fb9e1263d"
|
||||
integrity sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==
|
||||
|
||||
"@next/eslint-plugin-next@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.6.tgz#1d6d0fd21f58a650821bafeb3a205b25276ce5e3"
|
||||
integrity sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==
|
||||
"@next/eslint-plugin-next@15.1.7":
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.7.tgz#f8593c714f51ac6d443fb298584954d5def4392b"
|
||||
integrity sha512-kRP7RjSxfTO13NE317ek3mSGzoZlI33nc/i5hs1KaWpK+egs85xg0DJ4p32QEiHnR0mVjuUfhRIun7awqfL7pQ==
|
||||
dependencies:
|
||||
fast-glob "3.3.1"
|
||||
|
||||
"@next/swc-darwin-arm64@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz#92f99badab6cb41f4c5c11a3feffa574bd6a9276"
|
||||
integrity sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==
|
||||
"@next/swc-darwin-arm64@15.1.7":
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.7.tgz#ecc6eacf174df36a6c73f7c319ed864ec6e08079"
|
||||
integrity sha512-hPFwzPJDpA8FGj7IKV3Yf1web3oz2YsR8du4amKw8d+jAOHfYHYFpMkoF6vgSY4W6vB29RtZEklK9ayinGiCmQ==
|
||||
|
||||
"@next/swc-darwin-x64@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz#f56f4f8d5f6cb5d3915912ac95590d387f897da5"
|
||||
integrity sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==
|
||||
"@next/swc-darwin-x64@15.1.7":
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.7.tgz#d25b4c131d13439ea4b263dbcd0fd518a835f31c"
|
||||
integrity sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz#0aaffae519c93d1006419d7b98c34ebfd80ecacd"
|
||||
integrity sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==
|
||||
"@next/swc-linux-arm64-gnu@15.1.7":
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.7.tgz#b19abc7b56918042b5309f55f7010e7932ee4967"
|
||||
integrity sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==
|
||||
|
||||
"@next/swc-linux-arm64-musl@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz#e7398d3d31ca60033f708a718cd6c31edcee2e9a"
|
||||
integrity sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==
|
||||
"@next/swc-linux-arm64-musl@15.1.7":
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.7.tgz#cb2ac35d3024e9d46ce0d4ff03bf491e0773519f"
|
||||
integrity sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==
|
||||
|
||||
"@next/swc-linux-x64-gnu@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz#d76c72508f4d79d6016cab0c52640b93e590cffb"
|
||||
integrity sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==
|
||||
"@next/swc-linux-x64-gnu@15.1.7":
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.7.tgz#cf6e338a1fbb1c9b019c158a76a7ab4f143929ce"
|
||||
integrity sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==
|
||||
|
||||
"@next/swc-linux-x64-musl@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz#0b8ba80a53e65bf8970ed11ea923001e2512c7cb"
|
||||
integrity sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==
|
||||
"@next/swc-linux-x64-musl@15.1.7":
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.7.tgz#94c9117ece8e5851e7e6674f12d6b82b435e3a6f"
|
||||
integrity sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz#81b5dbbfdada2c05deef688e799af4a24097b65f"
|
||||
integrity sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==
|
||||
"@next/swc-win32-arm64-msvc@15.1.7":
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.7.tgz#4947f3b7f41c7347114985bf3c91e2eacddfe124"
|
||||
integrity sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==
|
||||
|
||||
"@next/swc-win32-x64-msvc@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz#131993c45ffd124fb4b15258e2f3f9669c143e3c"
|
||||
integrity sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==
|
||||
"@next/swc-win32-x64-msvc@15.1.7":
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.7.tgz#0adb399deb15291b61be94909a97e0d6ce1f61fa"
|
||||
integrity sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -3347,10 +3347,10 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@tanstack/eslint-plugin-query@5.66.0":
|
||||
version "5.66.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.66.0.tgz#c4537d62ca8bb94fbd7f0b1323053b03a79b39b0"
|
||||
integrity sha512-CzZhBxicLDuuSJbkZ4nPcuBqWnhLu72Zt9p/7qLQ93BepVnZJV6ZDlBLBuN5eg7YRACwECPLsntnwo1zuhgseQ==
|
||||
"@tanstack/eslint-plugin-query@5.66.1":
|
||||
version "5.66.1"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.66.1.tgz#9951e4d3633ca6248196742cad437b4d7c760cd7"
|
||||
integrity sha512-pYMVTGgJ7yPk9Rm6UWEmbY6TX0EmMmxJqYkthgeDCwEznToy2m+W928nUODFirtZBZlhBsqHy33LO0kyTlgf0w==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^8.18.1"
|
||||
|
||||
@@ -3565,10 +3565,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
|
||||
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
|
||||
|
||||
"@types/node@*", "@types/node@22.13.0":
|
||||
version "22.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.0.tgz#d376dd9a0ee2f9382d86c2d5d7beb4d198b4ea8c"
|
||||
integrity sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==
|
||||
"@types/node@*", "@types/node@22.13.1":
|
||||
version "22.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33"
|
||||
integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
@@ -3648,20 +3648,20 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz#63a1b0d24d85a971949f8d71d693019f58d2e861"
|
||||
integrity sha512-4Uta6REnz/xEJMvwf72wdUnC3rr4jAQf5jnTkeRQ9b6soxLxhDEbS/pfMPoJLDfFPNVRdryqWUIV/2GZzDJFZw==
|
||||
"@typescript-eslint/eslint-plugin@8.24.0":
|
||||
version "8.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz#574a95d67660a1e4544ae131d672867a5b40abb3"
|
||||
integrity sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.22.0"
|
||||
"@typescript-eslint/type-utils" "8.22.0"
|
||||
"@typescript-eslint/utils" "8.22.0"
|
||||
"@typescript-eslint/visitor-keys" "8.22.0"
|
||||
"@typescript-eslint/scope-manager" "8.24.0"
|
||||
"@typescript-eslint/type-utils" "8.24.0"
|
||||
"@typescript-eslint/utils" "8.24.0"
|
||||
"@typescript-eslint/visitor-keys" "8.24.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.3.1"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.0.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.13.0"
|
||||
@@ -3678,15 +3678,15 @@
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/parser@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.22.0.tgz#f21c5db24271f182ebbb4ba8c7ad3eb76e5f5f3a"
|
||||
integrity sha512-MqtmbdNEdoNxTPzpWiWnqNac54h8JDAmkWtJExBVVnSrSmi9z+sZUt0LfKqk9rjqmKOIeRhO4fHHJ1nQIjduIQ==
|
||||
"@typescript-eslint/parser@8.24.0":
|
||||
version "8.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.24.0.tgz#bba837f9ee125b78f459ad947ff9b61be8139085"
|
||||
integrity sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.22.0"
|
||||
"@typescript-eslint/types" "8.22.0"
|
||||
"@typescript-eslint/typescript-estree" "8.22.0"
|
||||
"@typescript-eslint/visitor-keys" "8.22.0"
|
||||
"@typescript-eslint/scope-manager" "8.24.0"
|
||||
"@typescript-eslint/types" "8.24.0"
|
||||
"@typescript-eslint/typescript-estree" "8.24.0"
|
||||
"@typescript-eslint/visitor-keys" "8.24.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
@@ -3724,13 +3724,13 @@
|
||||
"@typescript-eslint/types" "8.19.1"
|
||||
"@typescript-eslint/visitor-keys" "8.19.1"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz#e85836ddeb8eae715f870628bcc32fe96aaf4d0e"
|
||||
integrity sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==
|
||||
"@typescript-eslint/scope-manager@8.24.0":
|
||||
version "8.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz#2e34b3eb2ce768f2ffb109474174ced5417002b1"
|
||||
integrity sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.22.0"
|
||||
"@typescript-eslint/visitor-keys" "8.22.0"
|
||||
"@typescript-eslint/types" "8.24.0"
|
||||
"@typescript-eslint/visitor-keys" "8.24.0"
|
||||
|
||||
"@typescript-eslint/type-utils@8.13.0":
|
||||
version "8.13.0"
|
||||
@@ -3742,15 +3742,15 @@
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/type-utils@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.22.0.tgz#cd9f23c23f021357ef0baa3490d4d96edcc97509"
|
||||
integrity sha512-NzE3aB62fDEaGjaAYZE4LH7I1MUwHooQ98Byq0G0y3kkibPJQIXVUspzlFOmOfHhiDLwKzMlWxaNv+/qcZurJA==
|
||||
"@typescript-eslint/type-utils@8.24.0":
|
||||
version "8.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz#6ee3ec4db06f9e5e7b01ca6c2b5dd5843a9fd1e8"
|
||||
integrity sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "8.22.0"
|
||||
"@typescript-eslint/utils" "8.22.0"
|
||||
"@typescript-eslint/typescript-estree" "8.24.0"
|
||||
"@typescript-eslint/utils" "8.24.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.0.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/types@8.13.0":
|
||||
version "8.13.0"
|
||||
@@ -3767,10 +3767,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.19.1.tgz#015a991281754ed986f2e549263a1188d6ed0a8c"
|
||||
integrity sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==
|
||||
|
||||
"@typescript-eslint/types@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.22.0.tgz#d9dec7116479ad03aeb6c8ac9c5223c4c79cf360"
|
||||
integrity sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==
|
||||
"@typescript-eslint/types@8.24.0":
|
||||
version "8.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.24.0.tgz#694e7fb18d70506c317b816de9521300b0f72c8e"
|
||||
integrity sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.13.0":
|
||||
version "8.13.0"
|
||||
@@ -3814,19 +3814,19 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.0.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz#c188c3e19529d5b3145577c0bd967e2683b114df"
|
||||
integrity sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==
|
||||
"@typescript-eslint/typescript-estree@8.24.0":
|
||||
version "8.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz#0487349be174097bb329a58273100a9629e03c6c"
|
||||
integrity sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.22.0"
|
||||
"@typescript-eslint/visitor-keys" "8.22.0"
|
||||
"@typescript-eslint/types" "8.24.0"
|
||||
"@typescript-eslint/visitor-keys" "8.24.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.0.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/utils@8.13.0", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.13.0"
|
||||
@@ -3838,15 +3838,15 @@
|
||||
"@typescript-eslint/types" "8.13.0"
|
||||
"@typescript-eslint/typescript-estree" "8.13.0"
|
||||
|
||||
"@typescript-eslint/utils@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.22.0.tgz#c8cc4e52a9c711af8a741a82dc5d7242b7a8dd44"
|
||||
integrity sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==
|
||||
"@typescript-eslint/utils@8.24.0":
|
||||
version "8.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.24.0.tgz#21cb1195ae79230af825bfeed59574f5cb70a749"
|
||||
integrity sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "8.22.0"
|
||||
"@typescript-eslint/types" "8.22.0"
|
||||
"@typescript-eslint/typescript-estree" "8.22.0"
|
||||
"@typescript-eslint/scope-manager" "8.24.0"
|
||||
"@typescript-eslint/types" "8.24.0"
|
||||
"@typescript-eslint/typescript-estree" "8.24.0"
|
||||
|
||||
"@typescript-eslint/utils@^8.15.0":
|
||||
version "8.16.0"
|
||||
@@ -3892,12 +3892,12 @@
|
||||
"@typescript-eslint/types" "8.19.1"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz#02cc005014c372033eb9171e2275b76cba722a3f"
|
||||
integrity sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==
|
||||
"@typescript-eslint/visitor-keys@8.24.0":
|
||||
version "8.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz#36ecf0b9b1d819ad88a3bd4157ab7d594cb797c9"
|
||||
integrity sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.22.0"
|
||||
"@typescript-eslint/types" "8.24.0"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@ungap/structured-clone@^1.2.0":
|
||||
@@ -5449,12 +5449,12 @@ escodegen@^2.0.0:
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
eslint-config-next@15.1.6:
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.1.6.tgz#c056b7325dc70a247895c7c85515ebaae2bab35d"
|
||||
integrity sha512-Wd1uy6y7nBbXUSg9QAuQ+xYEKli5CgUhLjz1QHW11jLDis5vK5XB3PemL6jEmy7HrdhaRFDz+GTZ/3FoH+EUjg==
|
||||
eslint-config-next@15.1.7:
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.1.7.tgz#80d287e6d1c0742304de8fe57cbc45000c455c11"
|
||||
integrity sha512-zXoMnYUIy3XHaAoOhrcYkT9UQWvXqWju2K7NNsmb5wd/7XESDwof61eUdW4QhERr3eJ9Ko/vnXqIrj8kk/drYw==
|
||||
dependencies:
|
||||
"@next/eslint-plugin-next" "15.1.6"
|
||||
"@next/eslint-plugin-next" "15.1.7"
|
||||
"@rushstack/eslint-patch" "^1.10.3"
|
||||
"@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
"@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
@@ -7758,12 +7758,12 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
next@15.1.6:
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.1.6.tgz#ce22fd0a8f36da1fc4aba86e3ec7e98eb248c555"
|
||||
integrity sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==
|
||||
next@15.1.7:
|
||||
version "15.1.7"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.1.7.tgz#e814845e7cdb0294aee88ceab0bb962de83e8e6f"
|
||||
integrity sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==
|
||||
dependencies:
|
||||
"@next/env" "15.1.6"
|
||||
"@next/env" "15.1.7"
|
||||
"@swc/counter" "0.1.3"
|
||||
"@swc/helpers" "0.5.15"
|
||||
busboy "1.6.0"
|
||||
@@ -7771,14 +7771,14 @@ next@15.1.6:
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.6"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "15.1.6"
|
||||
"@next/swc-darwin-x64" "15.1.6"
|
||||
"@next/swc-linux-arm64-gnu" "15.1.6"
|
||||
"@next/swc-linux-arm64-musl" "15.1.6"
|
||||
"@next/swc-linux-x64-gnu" "15.1.6"
|
||||
"@next/swc-linux-x64-musl" "15.1.6"
|
||||
"@next/swc-win32-arm64-msvc" "15.1.6"
|
||||
"@next/swc-win32-x64-msvc" "15.1.6"
|
||||
"@next/swc-darwin-arm64" "15.1.7"
|
||||
"@next/swc-darwin-x64" "15.1.7"
|
||||
"@next/swc-linux-arm64-gnu" "15.1.7"
|
||||
"@next/swc-linux-arm64-musl" "15.1.7"
|
||||
"@next/swc-linux-x64-gnu" "15.1.7"
|
||||
"@next/swc-linux-x64-musl" "15.1.7"
|
||||
"@next/swc-win32-arm64-msvc" "15.1.7"
|
||||
"@next/swc-win32-x64-msvc" "15.1.7"
|
||||
sharp "^0.33.5"
|
||||
|
||||
no-case@^3.0.4:
|
||||
@@ -8146,14 +8146,14 @@ postcss@8.4.31:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@8.4.38:
|
||||
version "8.4.38"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||
postcss@8.4.49:
|
||||
version "8.4.49"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19"
|
||||
integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.2.0"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
postcss@^8.5.1:
|
||||
version "8.5.1"
|
||||
@@ -8176,10 +8176,10 @@ prettier-linter-helpers@^1.0.0:
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@3.4.2:
|
||||
version "3.4.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f"
|
||||
integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==
|
||||
prettier@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.0.tgz#50325a28887c6dfdf2ca3f8eaba02b66a8429ca7"
|
||||
integrity sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==
|
||||
|
||||
pretty-format@^27.0.2:
|
||||
version "27.5.1"
|
||||
@@ -8768,10 +8768,10 @@ safe-regex-test@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sass@1.83.4:
|
||||
version "1.83.4"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.4.tgz#5ccf60f43eb61eeec300b780b8dcb85f16eec6d1"
|
||||
integrity sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==
|
||||
sass@1.84.0:
|
||||
version "1.84.0"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.84.0.tgz#da9154cbccb2d2eac7a9486091b6d9ba93ef5bad"
|
||||
integrity sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==
|
||||
dependencies:
|
||||
chokidar "^4.0.0"
|
||||
immutable "^5.0.2"
|
||||
@@ -8979,7 +8979,7 @@ sort-keys@^5.0.0:
|
||||
dependencies:
|
||||
is-plain-obj "^4.0.0"
|
||||
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1:
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
@@ -9194,17 +9194,17 @@ strip-json-comments@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
styled-components@6.1.14:
|
||||
version "6.1.14"
|
||||
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.14.tgz#addd3b645f400f8aad84c992afec58804ad1d217"
|
||||
integrity sha512-KtfwhU5jw7UoxdM0g6XU9VZQFV4do+KrM8idiVCH5h4v49W+3p3yMe0icYwJgZQZepa5DbH04Qv8P0/RdcLcgg==
|
||||
styled-components@6.1.15:
|
||||
version "6.1.15"
|
||||
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.15.tgz#7651904d5424d08c1046056eb39024cc23c72ab7"
|
||||
integrity sha512-PpOTEztW87Ua2xbmLa7yssjNyUF9vE7wdldRfn1I2E6RTkqknkBYpj771OxM/xrvRGinLy2oysa7GOd7NcZZIA==
|
||||
dependencies:
|
||||
"@emotion/is-prop-valid" "1.2.2"
|
||||
"@emotion/unitless" "0.8.1"
|
||||
"@types/stylis" "4.2.5"
|
||||
css-to-react-native "3.2.0"
|
||||
csstype "3.1.3"
|
||||
postcss "8.4.38"
|
||||
postcss "8.4.49"
|
||||
shallowequal "1.1.0"
|
||||
stylis "4.3.2"
|
||||
tslib "2.6.2"
|
||||
@@ -9465,6 +9465,11 @@ ts-api-utils@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.0.tgz#b9d7d5f7ec9f736f4d0f09758b8607979044a900"
|
||||
integrity sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==
|
||||
|
||||
ts-api-utils@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.1.tgz#660729385b625b939aaa58054f45c058f33f10cd"
|
||||
integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==
|
||||
|
||||
ts-jest@29.2.5:
|
||||
version "29.2.5"
|
||||
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.5.tgz#591a3c108e1f5ebd013d3152142cb5472b399d63"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: desk
|
||||
version: 0.0.1
|
||||
version: 0.0.2
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
| Name | Description | Value |
|
||||
| ----------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `backend.pdb.enabled` | Enable pdb on backend | `true` |
|
||||
| `backend.dpAnnotations` | Annotations to add to the backend Deployment | `{}` |
|
||||
| `backend.command` | Override the backend container command | `[]` |
|
||||
| `backend.args` | Override the backend container args | `[]` |
|
||||
@@ -86,6 +87,7 @@
|
||||
|
||||
| Name | Description | Value |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- | ------------------------- |
|
||||
| `frontend.pdb.enabled` | Enable pdb on backend | `true` |
|
||||
| `frontend.image.repository` | Repository to use to pull desk's frontend container image | `lasuite/people-frontend` |
|
||||
| `frontend.image.tag` | desk's frontend container tag | `latest` |
|
||||
| `frontend.image.pullPolicy` | frontend container image pull policy | `IfNotPresent` |
|
||||
|
||||
@@ -138,3 +138,16 @@ spec:
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ if .Values.backend.pdb.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "desk.common.selectorLabels" (list . $component) | nindent 6 }}
|
||||
{{ end }}
|
||||
|
||||
@@ -138,3 +138,16 @@ spec:
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ if .Values.frontend.pdb.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "desk.common.selectorLabels" (list . $component) | nindent 6 }}
|
||||
{{ end }}
|
||||
|
||||
@@ -72,6 +72,10 @@ ingressAdmin:
|
||||
## @section backend
|
||||
|
||||
backend:
|
||||
## @param backend.pdb.enabled Enable pdb on backend
|
||||
pdb:
|
||||
enabled: true
|
||||
|
||||
## @param backend.dpAnnotations Annotations to add to the backend Deployment
|
||||
dpAnnotations: {}
|
||||
|
||||
@@ -179,6 +183,10 @@ backend:
|
||||
|
||||
## @section frontend
|
||||
frontend:
|
||||
## @param frontend.pdb.enabled Enable pdb on backend
|
||||
pdb:
|
||||
enabled: true
|
||||
|
||||
## @param frontend.image.repository Repository to use to pull desk's frontend container image
|
||||
## @param frontend.image.tag desk's frontend container tag
|
||||
## @param frontend.image.pullPolicy frontend container image pull policy
|
||||
|
||||
@@ -14,7 +14,7 @@ backend:
|
||||
DJANGO_SUPERUSER_PASSWORD: admin
|
||||
DJANGO_SUPERUSER_EMAIL: admin@example.com
|
||||
DJANGO_EMAIL_HOST_PASSWORD: changeme
|
||||
DJANGO_EMAIL_HOST: "mailcatcher"
|
||||
DJANGO_EMAIL_HOST: "maildev"
|
||||
DJANGO_EMAIL_PORT: 1025
|
||||
DJANGO_EMAIL_USE_SSL: False
|
||||
OIDC_RS_CLIENT_ID: changeme
|
||||
@@ -32,7 +32,6 @@ backend:
|
||||
OIDC_RP_SCOPES: "openid email siret"
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://desk.127.0.0.1.nip.io
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||
ORGANIZATION_PLUGINS: "plugins.organizations.NameFromSiretOrganizationPlugin"
|
||||
ORGANIZATION_REGISTRATION_ID_VALIDATORS: '[{"NAME": "django.core.validators.RegexValidator", "OPTIONS": {"regex": "^[0-9]{14}$"}}]'
|
||||
LOGIN_REDIRECT_URL: https://desk.127.0.0.1.nip.io
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://desk.127.0.0.1.nip.io
|
||||
@@ -101,12 +100,3 @@ ingress:
|
||||
ingressAdmin:
|
||||
enabled: true
|
||||
host: desk.127.0.0.1.nip.io
|
||||
|
||||
mailcatcher:
|
||||
image:
|
||||
repository: sj26/mailcatcher
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
ingress:
|
||||
enabled: true
|
||||
host: mailcatcher.127.0.0.1.nip.io
|
||||
|
||||
@@ -29,7 +29,7 @@ backend:
|
||||
DJANGO_SETTINGS_MODULE: people.settings
|
||||
DJANGO_SUPERUSER_EMAIL: admin@example.com
|
||||
DJANGO_SUPERUSER_PASSWORD: admin
|
||||
DJANGO_EMAIL_HOST: "mailcatcher"
|
||||
DJANGO_EMAIL_HOST: "maildev"
|
||||
DJANGO_EMAIL_PORT: 1025
|
||||
DJANGO_EMAIL_USE_SSL: False
|
||||
OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
|
||||
@@ -47,10 +47,10 @@ backend:
|
||||
name: backend
|
||||
key: OIDC_RP_CLIENT_SECRET
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email siret"
|
||||
OIDC_RP_SCOPES: "openid email siret given_name usual_name"
|
||||
USER_OIDC_FIELDS_TO_NAME: "given_name,usual_name"
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://desk.127.0.0.1.nip.io
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||
ORGANIZATION_PLUGINS: "plugins.organizations.NameFromSiretOrganizationPlugin"
|
||||
ORGANIZATION_REGISTRATION_ID_VALIDATORS: '[{"NAME": "django.core.validators.RegexValidator", "OPTIONS": {"regex": "^[0-9]{14}$"}}]'
|
||||
LOGIN_REDIRECT_URL: https://desk.127.0.0.1.nip.io
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://desk.127.0.0.1.nip.io
|
||||
@@ -107,12 +107,3 @@ ingress:
|
||||
ingressAdmin:
|
||||
enabled: true
|
||||
host: desk.127.0.0.1.nip.io
|
||||
|
||||
mailcatcher:
|
||||
image:
|
||||
repository: sj26/mailcatcher
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
ingress:
|
||||
enabled: true
|
||||
host: mailcatcher.127.0.0.1.nip.io
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ingress:
|
||||
enabled: true
|
||||
host: maildev.127.0.0.1.nip.io
|
||||
@@ -100,10 +100,10 @@ releases:
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.desk.yaml.gotmpl
|
||||
|
||||
- name: mailcatcher
|
||||
- name: maildev
|
||||
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
|
||||
missingFileHandler: Warn
|
||||
namespace: {{ .Namespace }}
|
||||
chart: ./mailcatcher
|
||||
chart: ./maildev
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.desk.yaml.gotmpl
|
||||
- env.d/values.maildev.yaml.gotmpl
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: mailcatcher
|
||||
description: A simple SMTP server with a web interface for testing email in development
|
||||
version: 1.0.0
|
||||
type: application
|
||||
@@ -1,25 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mailcatcher
|
||||
labels:
|
||||
app: mailcatcher
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mailcatcher
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mailcatcher
|
||||
spec:
|
||||
containers:
|
||||
- name: mailcatcher
|
||||
image: "{{ .Values.mailcatcher.image.repository }}:{{ .Values.mailcatcher.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.mailcatcher.image.pullPolicy }}
|
||||
ports:
|
||||
- name: smtp
|
||||
containerPort: 1025
|
||||
- name: http
|
||||
containerPort: 1080
|
||||
@@ -1,20 +0,0 @@
|
||||
{{- if .Values.mailcatcher.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: mailcatcher
|
||||
labels:
|
||||
app: mailcatcher
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.mailcatcher.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: mailcatcher
|
||||
port:
|
||||
number: 1080
|
||||
{{- end }}
|
||||
@@ -1,19 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mailcatcher
|
||||
labels:
|
||||
app: mailcatcher
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 1025
|
||||
targetPort: smtp
|
||||
protocol: TCP
|
||||
name: smtp
|
||||
- port: 1080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: mailcatcher
|
||||
@@ -1,16 +0,0 @@
|
||||
image:
|
||||
repository: sj26/mailcatcher
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
host: ""
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: maildev
|
||||
description: SMTP Server + Web Interface for viewing and testing emails during development.
|
||||
version: 1.0.0
|
||||
type: application
|
||||
@@ -0,0 +1,64 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: maildev
|
||||
labels:
|
||||
app: maildev
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: maildev
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: maildev
|
||||
spec:
|
||||
containers:
|
||||
- name: maildev
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: smtp-port
|
||||
containerPort: {{ .Values.ports.smtp }}
|
||||
protocol: TCP
|
||||
- name: web-port
|
||||
containerPort: {{ .Values.ports.web }}
|
||||
protocol: TCP
|
||||
env:
|
||||
{{- if or .Values.web.user .Values.web.pass}}
|
||||
- name: MAILDEV_WEB_USER
|
||||
value: "{{ .Values.web.user }}"
|
||||
- name: MAILDEV_WEB_PASS
|
||||
value: "{{ .Values.web.pass }}"
|
||||
{{- end }}
|
||||
- name: MAILDEV_SMTP_PORT
|
||||
value: "{{ .Values.ports.smtp }}"
|
||||
- name: MAILDEV_WEB_PORT
|
||||
value: "{{ .Values.ports.web }}"
|
||||
- name: MAILDEV_HTTPS
|
||||
value: "{{ .Values.https.enabled }}"
|
||||
{{- if .Values.https.key }}
|
||||
- name: MAILDEV_HTTPS_KEY
|
||||
value: "{{ .Values.https.key }}"
|
||||
{{- end }}
|
||||
{{- if .Values.https.cert }}
|
||||
- name: MAILDEV_HTTPS_CERT
|
||||
value: "{{ .Values.https.cert }}"
|
||||
{{- end }}
|
||||
{{- if .Values.incoming }}
|
||||
- name: MAILDEV_INCOMING_USER
|
||||
value: "{{ .Values.incoming.user }}"
|
||||
- name: MAILDEV_INCOMING_PASS
|
||||
value: "{{ .Values.incoming.pass }}"
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: {{ .Values.ports.web }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: {{ .Values.ports.web }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
@@ -0,0 +1,20 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: maildev
|
||||
labels:
|
||||
app: maildev
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingress.path }}
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: maildev-web
|
||||
port:
|
||||
number: {{ .Values.ports.web }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.ports.smtp }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: maildev-smtp
|
||||
labels:
|
||||
app: maildev
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: smtp-{{ .Values.ports.smtp }}-tcp
|
||||
port: {{ .Values.ports.smtp }}
|
||||
protocol: TCP
|
||||
targetPort: {{ .Values.ports.smtp }}
|
||||
selector:
|
||||
app: maildev
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.ports.web }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: maildev-web
|
||||
labels:
|
||||
app: maildev
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: web-{{ .Values.ports.web }}-tcp
|
||||
port: {{ .Values.ports.web }}
|
||||
protocol: TCP
|
||||
targetPort: {{ .Values.ports.web }}
|
||||
selector:
|
||||
app: maildev
|
||||
{{- end }}
|
||||
@@ -0,0 +1,40 @@
|
||||
image:
|
||||
repository: maildev/maildev
|
||||
tag: "2.2.1"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
ports:
|
||||
# MAILDEV_SMTP_PORT
|
||||
smtp: 1025
|
||||
# MAILDEV_WEB_PORT
|
||||
web: 1080
|
||||
|
||||
# MAILDEV_HTTPS
|
||||
https:
|
||||
enabled:
|
||||
# MAILDEV_HTTPS_KEY
|
||||
# key:
|
||||
# MAILDEV_HTTPS_KEY
|
||||
# cert:
|
||||
|
||||
# Web interface
|
||||
web:
|
||||
# MAILDEV_DISABLE_WEB
|
||||
disable: false
|
||||
# MAILDEV_WEB_USER
|
||||
user:
|
||||
# MAILDEV_WEB_PASS
|
||||
pass:
|
||||
|
||||
incoming:
|
||||
# MAILDEV_INCOMING_USER
|
||||
# user:
|
||||
# MAILDEV_INCOMING_PASS
|
||||
# pass:
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
host: ""
|
||||
path: /
|
||||
|
||||
resources:
|
||||
@@ -16,7 +16,7 @@
|
||||
"volta": {
|
||||
"node": "16.15.1"
|
||||
},
|
||||
"repository": "https://github.com/numerique-gouv/people",
|
||||
"repository": "https://github.com/suitenumerique/people",
|
||||
"author": "DINUM",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/numerique-gouv/people.git"
|
||||
"url": "git+https://github.com/suitenumerique/people.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "DINUM",
|
||||
@@ -16,9 +16,9 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/numerique-gouv/people/issues"
|
||||
"url": "https://github.com/suitenumerique/people/issues"
|
||||
},
|
||||
"homepage": "https://github.com/numerique-gouv/people#readme",
|
||||
"homepage": "https://github.com/suitenumerique/people#readme",
|
||||
"devDependencies": {
|
||||
"openapi-typescript-codegen": "0.29.0"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user