Compare commits

...

25 Commits

Author SHA1 Message Date
Laurent Bossavit 38cfaff81f Fix typo 2024-10-25 13:00:37 +02:00
Laurent Bossavit 80ac55b6e2 (ci) add docker layer caching 2024-10-24 16:32:13 +02:00
Sabrina Demagny 230ed75d8d 🔖(patch) release version 1.4.1
Update all version files and changelog for patch release.
2024-10-23 22:55:56 +02:00
NathanPanchout 5ac3454c5a 🚑️(frontend) fix MailDomainsLayout
The content is not scrollable. We add an auto overflow to solve this problem
2024-10-23 21:44:28 +02:00
Sabrina Demagny bbac34b62a 🔖(minor) release version 1.4.0 (#483)
Update all version files and changelog for minor release.
2024-10-23 19:15:14 +02:00
Sabrina Demagny 38508c272e 📝(CHANGELOG) fix modification declaration
One of unreleased modification declaration was
declared in a wrong place
2024-10-23 18:46:04 +02:00
Sabrina Demagny 10a81f14e6 ✏️(mail) fix typo in mailbox creation email
Whitespace missing in french translation after ':'
2024-10-23 17:56:40 +02:00
Nathan Panchout a08689a64d (frontend) add tabs for mail domain page (#466)
Currently, it is complicated to understand the navigation between mailbox
management and role management for an email domain.
This is why we add tabs with explicit naming
2024-10-23 17:45:42 +02:00
Marie PUPO JEAMMET 30229e11f9 🐛(sentry) fix duplicated sentry errors
errors were sent to sentry twice
2024-10-22 18:03:01 +02:00
Sabrina Demagny d11d1f3bd0 🐛(script) improve and fix release script
Use regex to replace version and add missing
frontend update version.
2024-10-22 00:28:08 +02:00
renovate[bot] 6a22169dae ⬆️(dependencies) update python dependencies 2024-10-21 12:22:49 +02:00
Marie PUPO JEAMMET 6e7a6e9d51 🔖(patch) release version 1.3.1
Update all version files and changelog for patch release.
2024-10-18 14:49:56 +02:00
Marie PUPO JEAMMET 95fb476041 🔖(minor) release version 1.3.0
Update all version files and changelog for minor release.
2024-10-18 11:53:36 +02:00
Sabrina Demagny 6b4ea1a2e7 💄(mail) improve mailbox creation email
Remove useless icons, modify text
and improve displaying.
2024-10-15 13:34:03 +02:00
Sabrina Demagny be55d6ea09 📝(CHANGELOG) fix some modifications declaration
Some unreleased declarations was declared in a wrong place
2024-10-15 12:55:27 +02:00
Nathan Panchout 9c9216bb51 (frontend) show version number in footer (#461)
In order to see the version number pushed into production
2024-10-14 16:54:21 +02:00
Sabrina Demagny 017f52a0dc (api) add RELEASE version on config endpoint
Add release version deployed to config endpoint
in order to display release info in La Régie footer.
2024-10-14 14:57:28 +02:00
Quentin BEY d635c484ae 🛂(backend) do not duplicate user when disabled
When a user is disabled and tries to login, we
don't want the user to be duplicated,
the user should not be able to login.

Fixes https://github.com/numerique-gouv/people/issues/455
2024-10-11 16:19:18 +02:00
Marie PUPO JEAMMET 0e48bc0f90 🛂(backend) match email if no existing user matches the sub
Some OIDC identity providers may provide a random value in the "sub"
field instead of an identifying ID. In this case, it may be a good
idea to fallback to matching the user on its email field.
2024-10-11 15:24:53 +02:00
Sabrina Demagny f243a2423f (backend) manage roles on domain admin view
Allow to manage mail domain roles on mail domain admin interface
2024-10-10 16:09:37 +02:00
Marie PUPO JEAMMET 763eb254a8 📝(dimai) update docs on dimail
update README and docs describing interoperability between people and dimail

Co-authored-by: Laurent Bossavit <216452+Morendil@users.noreply.github.com>
2024-10-09 10:59:07 +02:00
Marie PUPO JEAMMET 9b613e63a9 🔧(sentry) reduce trace
we reduce trace to reduce spamming on sentry
2024-10-08 14:35:40 +02:00
Nathan Panchout 6b7f00651f 🐛(frontend) Bug manage access role (#449)
* 🐛(frontend) update access role

Currently, when you click on modify a role, the roles displayed are not the
correct ones. We always took the role of the connected user.
Additionally, there was a warning and a ban on changing roles if you were the
last owner, but it was decided that it was the backend that was throwing the
error and the action was allowed.

* (frontend) adapt tests

We adapt the tests. Remove the check if you are the last owner and use the new
props inside the relevant tests
2024-10-08 10:02:15 +02:00
renovate[bot] fe249c5b6f ⬆️(dependencies) update python dependencies 2024-10-07 17:56:01 +02:00
Marie PUPO JEAMMET 579657afa4 🐛(dimail) add status code in case of unexpected error
add status code in case of unexpected error
2024-10-04 11:32:49 +02:00
52 changed files with 1067 additions and 472 deletions
+18 -4
View File
@@ -150,7 +150,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set services env variables
run: |
make create-env-files
@@ -175,15 +175,29 @@ jobs:
with:
path: src/frontend/apps/desk/out/
key: build-front-${{ github.run_id }}
# Pull the latest image to build, and avoid caching pull-only images.
# (docker pull is faster than caching in most cases.)
- run: docker compose pull
# In this step, this action saves a list of existing images,
# the cache is created without them in the post run.
# It also restores the cache if it exists.
- uses: satackey/action-docker-layer-caching@v0.0.11
# Ignore the failure of a step and avoid terminating the job.
continue-on-error: true
- name: Build and Start Docker Servers
env:
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
run: |
docker compose build --pull --build-arg BUILDKIT_INLINE_CACHE=1
docker compose build --build-arg BUILDKIT_INLINE_CACHE=1
make run
# Finally, "Post Run satackey/action-docker-layer-caching@v0.0.11",
# which is the process of saving the cache, will be executed.
- name: Apply DRF migrations
run: |
make migrate
+43 -1
View File
@@ -8,6 +8,44 @@ and this project adheres to
## [Unreleased]
## [1.4.1] - 2024-10-23
### Fixed
- 🚑️(frontend) fix MailDomainsLayout
## [1.4.0] - 2024-10-23
### Added
- ✨(frontend) add tabs inside #466
### Fixed
- ✏️(mail) fix typo into mailbox creation email
- 🐛(sentry) fix duplicated sentry errors #479
- 🐛(script) improve and fix release script
## [1.3.1] - 2024-10-18
## [1.3.0] - 2024-10-18
### Added
- ✨(api) add RELEASE version on config endpoint #459
- ✨(backend) manage roles on domain admin view
- ✨(frontend) show version number in footer #369
### Changed
- 🛂(backend) match email if no existing user matches the sub
### Fixed
- 💄(mail) improve mailbox creation email #462
- 🐛(frontend) fix update accesses form #448
- 🛂(backend) do not duplicate user when disabled
## [1.2.1] - 2024-10-03
### Fixed
@@ -86,7 +124,11 @@ 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.2.1...main
[unreleased]: https://github.com/numerique-gouv/people/compare/v1.4.1...main
[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
+25 -18
View File
@@ -1,12 +1,12 @@
# People
People is an application to handle users and teams.
People is an application to handle users and teams, and distribute permissions accross [La Suite](https://lasuite.numerique.gouv.fr/).
As of today, this project is **not yet ready for production**. Expect breaking changes.
People is built on top of [Django Rest
It is built on top of [Django Rest
Framework](https://www.django-rest-framework.org/).
All interoperabilities will be described in `docs/interoperability`.
## Getting started
### Prerequisite
@@ -25,7 +25,7 @@ $ docker compose -v
> ⚠️ You may need to run the following commands with `sudo` but this can be
> avoided by assigning your user to the `docker` group.
### Project bootstrap
### Bootstrap project
The easiest way to start working on the project is to use GNU Make:
@@ -38,7 +38,7 @@ database migrations and compile translations. It's a good idea to use this
command each time you are pulling code from the project repository to avoid
dependency-related or migration-related issues.
Your Docker services should now be up and running 🎉
Your Docker services should now be up and running! 🎉
Note that if you need to run them afterward, you can use the eponym Make rule:
@@ -46,13 +46,7 @@ Note that if you need to run them afterward, you can use the eponym Make rule:
$ make run
```
### Adding content
You can create a basic demo site by running:
$ make demo
Finally, you can check all available Make rules using:
You can check all available Make rules using:
```bash
$ make help
@@ -71,6 +65,23 @@ $ make superuser
You can then login with sub `admin` and password `admin`.
### Adding demo content
You can create a basic demo site by running:
```bash
$ make demo
```
### Setting dimail database
To ease local development when working on interoperability between people and dimail, we embark dimail-api in a container running in "fake" mode.
To populate dimail local database with users/domains/permissions needed for basic development:
- log in with "people" user
- run `make dimail-setup-db`
### Run frontend
Run the front with:
@@ -79,14 +90,10 @@ Run the front with:
$ make run-front-desk
```
Then access at
[http://localhost:3000](http://localhost:3000)
Then access [http://localhost:3000](http://localhost:3000) with :
user: people
password: people
## Contributing
This project is intended to be community-driven, so please, do not hesitate to
+35
View File
@@ -0,0 +1,35 @@
# dimail
## What is dimail ?
The mailing solution provided in La Suite is [Open-XChange](https://www.open-xchange.com/) (OX).
OX not having a provisioning API, 'dimail-api' or 'dimail' was created to allow mail-provisioning through People.
API and its documentation can be found [here](https://api.dev.ox.numerique.gouv.fr/docs#/).
## Architectural links of dimail
As dimail's primary goal is to act as an interface between People and OX, its architecture is similar to that of People. A series of requests are sent from People to dimail upon creating domains, users and accesses.
### Domains
Upon creating a domain on People, the same domain is created on dimail and will undergo a series of checks. When all checks have passed, the domain is considered valid and mailboxes can be created on it.
### Users
The ones issuing requests. Dimail users will reflect domains owners and administrators on People, for logging purposes and to allow direct use of dimail, if desired. User reconciliation is made on user uuid provided by ProConnect.
### Accesses
As for People, an access - a permissions (or "allows" in dimail) - grants an user permission to create objects on a domain.
Permissions requests are sent automatically upon :
- dimail database initialisation:
+ permission for dimail user People to create users and domains
- domain creation :
+ permission for dimail user People to manage domain
+ permission for new owner on new domain
- user creation:
+ permission for People to manage user
- access creation, if owner or admin:
+ permission for this user to manage domain
+6 -7
View File
@@ -1,5 +1,6 @@
import datetime
import os
import re
import sys
from utils import run_command
@@ -17,9 +18,7 @@ def update_files(version):
lines = file.readlines()
for index, line in enumerate(lines):
if line.startswith("version = "):
last_version = line.split("=")[-1]
new_line = line.replace(last_version, f' "{version}"\n')
lines[index] = new_line
lines[index] = re.sub(r'\"(.*?)\"', f'"{version}"', line)
with open(path, 'w+') as file:
file.writelines(lines)
@@ -30,8 +29,8 @@ def update_files(version):
with open(path, 'r') as file:
lines = file.readlines()
for index, line in enumerate(lines):
if line.startswith(" tag: "):
lines[index] = f' tag: "v{version}"\n'
if "tag:" in line:
lines[index] = re.sub(r'\"(.*?)\"', f'"v{version}"', line)
with open(path, 'w+') as file:
file.writelines(lines)
@@ -47,8 +46,8 @@ def update_files(version):
with open(path, 'r') as file:
lines = file.readlines()
for index, line in enumerate(lines):
if line.startswith(' "version": '):
lines[index] = f' "version": "{version}",\n'
if "version" in line:
lines[index] = re.sub(r'"version": \"(.*?)\"', f'"version": "{version}"', line)
with open(path, 'w+') as file:
file.writelines(lines)
return
+1 -1
View File
@@ -506,7 +506,7 @@ class ConfigView(views.APIView):
GET /api/v1.0/config/
Return a dictionary of public settings.
"""
array_settings = ["LANGUAGES", "FEATURES"]
array_settings = ["LANGUAGES", "FEATURES", "RELEASE"]
dict_settings = {}
for setting in array_settings:
dict_settings[setting] = getattr(settings, setting)
+51 -19
View File
@@ -1,6 +1,7 @@
"""Authentication Backends for the People core app."""
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import SuspiciousOperation
from django.utils.translation import gettext_lazy as _
@@ -9,6 +10,8 @@ from mozilla_django_oidc.auth import (
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
)
User = get_user_model()
class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
"""Custom OpenID Connect (OIDC) Authentication Backend.
@@ -48,7 +51,7 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
return userinfo
def get_or_create_user(self, access_token, id_token, payload):
"""Return a User based on userinfo. Get or create a new user if no user matches the Sub.
"""Return a User based on userinfo. Create a new user if no match is found.
Parameters:
- access_token (str): The access token.
@@ -64,30 +67,30 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
user_info = self.get_userinfo(access_token, id_token, payload)
# Compute user name from OIDC name fields as defined in settings
names_list = [
user_info[field]
for field in settings.USER_OIDC_FIELDS_TO_NAME
if user_info.get(field)
]
user_info["name"] = " ".join(names_list) or None
# 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 = {
"email": email,
"name": full_name,
}
sub = user_info.get("sub")
if sub is None:
if not sub:
raise SuspiciousOperation(
_("User info contained no recognizable user identification")
)
try:
user = self.UserModel.objects.get(sub=sub, is_active=True)
except self.UserModel.DoesNotExist:
if self.get_settings("OIDC_CREATE_USER", True):
user = self.create_user(user_info)
else:
email = user_info.get("email")
name = user_info.get("name")
if email and email != user.email or name and name != user.name:
self.UserModel.objects.filter(sub=sub).update(email=email, name=name)
# if sub is absent, try matching on email
user = self.get_existing_user(sub, email)
if user:
if not user.is_active:
raise SuspiciousOperation(_("User account is disabled"))
self.update_user_if_needed(user, claims)
elif self.get_settings("OIDC_CREATE_USER", True):
user = User.objects.create(sub=sub, password="!", **claims) # noqa: S106
return user
@@ -105,3 +108,32 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
email=claims.get("email"),
name=claims.get("name"),
)
def compute_full_name(self, user_info):
"""Compute user's full name based on OIDC fields in settings."""
name_fields = settings.USER_OIDC_FIELDS_TO_NAME
full_name = " ".join(
user_info[field] for field in name_fields if user_info.get(field)
)
return full_name or None
def get_existing_user(self, sub, email):
"""Fetch existing user by sub or email."""
try:
return User.objects.get(sub=sub)
except User.DoesNotExist:
if email and settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION:
try:
return User.objects.get(email=email)
except User.DoesNotExist:
pass
return None
def update_user_if_needed(self, user, claims):
"""Update user claims if they have changed."""
has_changed = any(
value and value != getattr(user, key) for key, value in claims.items()
)
if has_changed:
updated_claims = {key: value for key, value in claims.items() if value}
self.UserModel.objects.filter(sub=user.sub).update(**updated_claims)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,5 +1,6 @@
"""Unit tests for the Authentication Backends."""
from django.contrib.auth import get_user_model
from django.core.exceptions import SuspiciousOperation
import pytest
@@ -8,6 +9,7 @@ from core import factories, models
from core.authentication.backends import OIDCAuthenticationBackend
pytestmark = pytest.mark.django_db
User = get_user_model()
def test_authentication_getter_existing_user_no_email(
@@ -101,6 +103,59 @@ def test_authentication_getter_existing_user_change_fields(
assert user.name == f"{first_name:s} {last_name:s}"
def test_authentication_getter_existing_user_via_email(
django_assert_num_queries, monkeypatch
):
"""
If an existing user doesn't match the sub but matches the email,
the user should be returned.
"""
klass = OIDCAuthenticationBackend()
db_user = factories.UserFactory()
def get_userinfo_mocked(*args):
return {"sub": "123", "email": db_user.email}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with django_assert_num_queries(2):
user = klass.get_or_create_user(
access_token="test-token", id_token=None, payload=None
)
assert user == db_user
def test_authentication_getter_existing_user_no_fallback_to_email(
settings, monkeypatch
):
"""
When the "OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION" setting is set to False,
the system should not match users by email, even if the email matches.
"""
klass = OIDCAuthenticationBackend()
db_user = factories.UserFactory()
# Set the setting to False
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = False
def get_userinfo_mocked(*args):
return {"sub": "123", "email": db_user.email}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
user = klass.get_or_create_user(
access_token="test-token", id_token=None, payload=None
)
# Since the sub doesn't match, it should create a new user
assert models.User.objects.count() == 2
assert user != db_user
assert user.sub == "123"
def test_authentication_getter_new_user_no_email(monkeypatch):
"""
If no user matches the user's info sub, a user should be created.
@@ -169,3 +224,63 @@ def test_models_oidc_user_getter_invalid_token(django_assert_num_queries, monkey
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
assert models.User.objects.exists() is False
def test_authentication_getter_existing_disabled_user_via_sub(
django_assert_num_queries, monkeypatch
):
"""
If an existing user matches the sub but is disabled,
an error should be raised and a user should not be created.
"""
klass = OIDCAuthenticationBackend()
db_user = factories.UserFactory(name="John Doe", is_active=False)
def get_userinfo_mocked(*args):
return {
"sub": db_user.sub,
"email": db_user.email,
"first_name": "John",
"last_name": "Doe",
}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with (
django_assert_num_queries(1),
pytest.raises(SuspiciousOperation, match="User account is disabled"),
):
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
assert models.User.objects.count() == 1
def test_authentication_getter_existing_disabled_user_via_email(
django_assert_num_queries, monkeypatch
):
"""
If an existing user does not matches the sub but match the email and is disabled,
an error should be raised and a user should not be created.
"""
klass = OIDCAuthenticationBackend()
db_user = factories.UserFactory(name="John Doe", is_active=False)
def get_userinfo_mocked(*args):
return {
"sub": "random",
"email": db_user.email,
"first_name": "John",
"last_name": "Doe",
}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with (
django_assert_num_queries(2),
pytest.raises(SuspiciousOperation, match="User account is disabled"),
):
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
assert models.User.objects.count() == 1
@@ -21,6 +21,7 @@ def test_api_config_anonymous():
assert response.json() == {
"LANGUAGES": [["en-us", "English"], ["fr-fr", "French"]],
"FEATURES": {"TEAMS": True},
"RELEASE": "NA",
}
@@ -36,4 +37,5 @@ def test_api_config_authenticated():
assert response.json() == {
"LANGUAGES": [["en-us", "English"], ["fr-fr", "French"]],
"FEATURES": {"TEAMS": True},
"RELEASE": "NA",
}
+6
View File
@@ -4,6 +4,7 @@ from django.urls import path
from .views import (
DebugViewHtml,
DebugViewNewMailboxHtml,
DebugViewTxt,
)
@@ -18,4 +19,9 @@ urlpatterns = [
DebugViewTxt.as_view(),
name="debug.mail.invitation_txt",
),
path(
"__debug__/mail/new_mailbox_html",
DebugViewNewMailboxHtml.as_view(),
name="debug.mail.new_mailbox_html",
),
]
+14
View File
@@ -25,3 +25,17 @@ class DebugViewTxt(DebugBaseView):
"""Debug View for Text Email Layout"""
template_name = "mail/text/invitation.txt"
class DebugViewNewMailboxHtml(DebugBaseView):
"""Debug view for new mailbox email layout"""
template_name = "mail/html/new_mailbox.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["mailbox_data"] = {
"email": "john.doe@example.com",
"password": "6HGVAsjoog_v",
}
return context
Binary file not shown.
+247 -101
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-people\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-21 19:26+0000\n"
"POT-Creation-Date: 2024-10-15 10:52+0000\n"
"PO-Revision-Date: 2024-01-03 23:15\n"
"Last-Translator: \n"
"Language-Team: French\n"
@@ -17,212 +17,251 @@ msgstr ""
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 2\n"
#: core/admin.py:70
#: core/admin.py:45
msgid "Personal info"
msgstr ""
#: core/admin.py:72
#: core/admin.py:47
msgid "Permissions"
msgstr ""
#: core/admin.py:84
#: core/admin.py:59
msgid "Important dates"
msgstr ""
#: core/authentication.py:81
#: core/admin.py:97
msgid "User"
msgstr ""
#: core/authentication/backends.py:82
msgid "User info contained no recognizable user identification"
msgstr ""
#: core/authentication.py:114
#: core/authentication/backends.py:90
msgid "User account is disabled"
msgstr ""
#: core/authentication/backends.py:102
msgid "Claims contained no recognizable user identification"
msgstr ""
#: core/models.py:38
#: core/enums.py:23
msgid "Failure"
msgstr ""
#: core/enums.py:24 mailbox_manager/enums.py:20
msgid "Pending"
msgstr ""
#: core/enums.py:25
msgid "Success"
msgstr ""
#: core/models.py:42
msgid "Member"
msgstr ""
#: core/models.py:39
#: core/models.py:43 mailbox_manager/enums.py:13
msgid "Administrator"
msgstr ""
#: core/models.py:40
#: core/models.py:44 mailbox_manager/enums.py:14
msgid "Owner"
msgstr ""
#: core/models.py:52
#: core/models.py:56
msgid "id"
msgstr ""
#: core/models.py:53
#: core/models.py:57
msgid "primary key for the record as UUID"
msgstr ""
#: core/models.py:59
#: core/models.py:63
msgid "created at"
msgstr ""
#: core/models.py:60
#: core/models.py:64
msgid "date and time at which a record was created"
msgstr ""
#: core/models.py:65
#: core/models.py:69
msgid "updated at"
msgstr ""
#: core/models.py:66
#: core/models.py:70
msgid "date and time at which a record was last updated"
msgstr ""
#: core/models.py:97
#: core/models.py:101
msgid "full name"
msgstr ""
#: core/models.py:98
#: core/models.py:102
msgid "short name"
msgstr ""
#: core/models.py:103
#: core/models.py:107
msgid "contact information"
msgstr ""
#: core/models.py:104
#: core/models.py:108
msgid "A JSON object containing the contact information"
msgstr ""
#: core/models.py:118
#: core/models.py:122
msgid "contact"
msgstr ""
#: core/models.py:119
#: core/models.py:123
msgid "contacts"
msgstr ""
#: core/models.py:160 core/models.py:263 core/models.py:483
msgid "email address"
msgstr ""
#: core/models.py:172
msgid "language"
msgstr ""
#: core/models.py:173
msgid "The language in which the user wants to see the interface."
msgstr ""
#: core/models.py:179
msgid "The timezone in which the user wants to see times."
msgstr ""
#: core/models.py:182
msgid "device"
msgstr ""
#: core/models.py:184
msgid "Whether the user is a device or a real user."
msgstr ""
#: core/models.py:187
msgid "staff status"
msgstr ""
#: core/models.py:189
msgid "Whether the user can log into this admin site."
msgstr ""
#: core/models.py:192
msgid "active"
msgstr ""
#: core/models.py:195
msgid ""
"Whether this user should be treated as active. Unselect this instead of "
"deleting accounts."
msgstr ""
#: core/models.py:207
msgid "user"
msgstr ""
#: core/models.py:208
msgid "users"
msgstr ""
#: core/models.py:248
#: core/models.py:167
msgid ""
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
"_ characters."
msgstr ""
#: core/models.py:255
#: core/models.py:173
msgid "sub"
msgstr ""
#: core/models.py:257
#: core/models.py:175
msgid ""
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
"characters only."
msgstr ""
#: core/models.py:264
#: core/models.py:181 core/models.py:489
msgid "email address"
msgstr ""
#: core/models.py:182 mailbox_manager/models.py:20
msgid "name"
msgstr ""
#: core/models.py:266
msgid "main"
#: core/models.py:194
msgid "language"
msgstr ""
#: core/models.py:268
msgid "Designates whether the email is the main one."
#: core/models.py:195
msgid "The language in which the user wants to see the interface."
msgstr ""
#: core/models.py:274
msgid "identity"
#: core/models.py:201
msgid "The timezone in which the user wants to see times."
msgstr ""
#: core/models.py:275
msgid "identities"
#: core/models.py:204
msgid "device"
msgstr ""
#: core/models.py:282
msgid "This email address is already declared for this user."
#: core/models.py:206
msgid "Whether the user is a device or a real user."
msgstr ""
#: core/models.py:356
#: core/models.py:209
msgid "staff status"
msgstr ""
#: core/models.py:211
msgid "Whether the user can log into this admin site."
msgstr ""
#: core/models.py:214
msgid "active"
msgstr ""
#: core/models.py:217
msgid ""
"Whether this user should be treated as active. Unselect this instead of "
"deleting accounts."
msgstr ""
#: core/models.py:229
msgid "user"
msgstr ""
#: core/models.py:230
msgid "users"
msgstr ""
#: core/models.py:306
msgid "Team"
msgstr ""
#: core/models.py:357
#: core/models.py:307
msgid "Teams"
msgstr ""
#: core/models.py:417
#: core/models.py:367
msgid "Team/user relation"
msgstr ""
#: core/models.py:418
#: core/models.py:368
msgid "Team/user relations"
msgstr ""
#: core/models.py:423
#: core/models.py:373
msgid "This user is already in this team."
msgstr ""
#: core/models.py:500
#: core/models.py:462
msgid "url"
msgstr ""
#: core/models.py:463
msgid "secret"
msgstr ""
#: core/models.py:472
msgid "Team webhook"
msgstr ""
#: core/models.py:473
msgid "Team webhooks"
msgstr ""
#: core/models.py:506
msgid "Team invitation"
msgstr ""
#: core/models.py:501
#: core/models.py:507
msgid "Team invitations"
msgstr ""
#: core/models.py:526
#: core/models.py:532
msgid "This email is already associated to a registered user."
msgstr ""
#: core/models.py:568 core/models.py:573
#: core/models.py:574 core/models.py:580
msgid "Invitation to join Desk!"
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"
@@ -246,7 +285,7 @@ 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 Equipes, your new "
"We are delighted to welcome you to our community on Régie, your new "
"companion to simplify the management of your groups efficiently, "
"intuitively, and securely."
msgstr ""
@@ -260,7 +299,7 @@ msgstr ""
#: core/templates/mail/html/invitation.html:236
#: core/templates/mail/text/invitation.txt:16
msgid "With Equipes, you will be able to:"
msgid "With Régie, you will be able to:"
msgstr ""
#: core/templates/mail/html/invitation.html:237
@@ -294,13 +333,13 @@ msgstr ""
#: core/templates/mail/html/invitation.html:252
#: core/templates/mail/text/invitation.txt:23
msgid "Visit Equipes"
msgid "Visit Régie"
msgstr ""
#: core/templates/mail/html/invitation.html:261
#: core/templates/mail/text/invitation.txt:25
msgid ""
"We are confident that Equipes will help you increase efficiency and "
"We are confident that Régie will help you increase efficiency and "
"productivity while strengthening the bond among members."
msgstr ""
@@ -320,19 +359,126 @@ msgid ""
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 ""
msgstr "Cordialement,"
#: core/templates/mail/html/invitation.html:279
#: core/templates/mail/text/invitation.txt:31
msgid "The La Suite Numérique Team"
msgstr "L'équipe de La Suite Numérique"
#: core/templates/mail/html/new_mailbox.html:159
#: core/templates/mail/text/new_mailbox.txt:3
msgid "La Messagerie"
msgstr "La Messagerie"
#: core/templates/mail/html/new_mailbox.html:188
#: core/templates/mail/text/new_mailbox.txt:5
msgid "Welcome to La Messagerie"
msgstr "Bienvenue dans La Messagerie"
#: 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 "La Messagerie est la solution de mail de La Suite."
#: core/templates/mail/html/new_mailbox.html:199
#: core/templates/mail/text/new_mailbox.txt:7
msgid "Your mailbox has been created."
msgstr "Votre boîte mail a été créée."
#: core/templates/mail/html/new_mailbox.html:204
#: core/templates/mail/text/new_mailbox.txt:8
msgid "Please find below your login info: "
msgstr "Voici vos identifiants de connexion :"
#: core/templates/mail/html/new_mailbox.html:228
#: core/templates/mail/text/new_mailbox.txt:10
msgid "Email address: "
msgstr "Adresse email : "
#: 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 "Mot de passe temporaire (à modifier à la première connexion) : "
#: core/templates/mail/html/new_mailbox.html:261
#: core/templates/mail/text/new_mailbox.txt:13
msgid "Go to La Messagerie"
msgstr "Accéder à La Messagerie"
#: core/templates/mail/html/new_mailbox.html:273
#: core/templates/mail/text/new_mailbox.txt:17
msgid "La Suite Team"
msgstr "L'équipe de La Suite"
#: core/templates/mail/text/hello.txt:8
#, python-format
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
msgstr ""
#: people/settings.py:133
#: mailbox_manager/enums.py:12
msgid "Viewer"
msgstr ""
#: mailbox_manager/enums.py:21
msgid "Enabled"
msgstr ""
#: mailbox_manager/enums.py:22
msgid "Failed"
msgstr ""
#: mailbox_manager/enums.py:23
msgid "Disabled"
msgstr ""
#: mailbox_manager/models.py:31
msgid "Mail domain"
msgstr ""
#: mailbox_manager/models.py:32
msgid "Mail domains"
msgstr ""
#: mailbox_manager/models.py:98
msgid "User/mail domain relation"
msgstr ""
#: mailbox_manager/models.py:99
msgid "User/mail domain relations"
msgstr ""
#: mailbox_manager/models.py:171
msgid "local_part"
msgstr ""
#: mailbox_manager/models.py:185
msgid "secondary email address"
msgstr ""
#: mailbox_manager/models.py:190
msgid "Mailbox"
msgstr ""
#: mailbox_manager/models.py:191
msgid "Mailboxes"
msgstr ""
#: mailbox_manager/utils/dimail.py:137
msgid "Your new mailbox information"
msgstr "Informations concernant votre nouvelle boîte mail"
#: people/settings.py:134
msgid "English"
msgstr ""
#: people/settings.py:134
#: people/settings.py:135
msgid "French"
msgstr ""
#~ msgid "Regards,"
#~ msgstr "Cordialement,"
+9
View File
@@ -6,6 +6,14 @@ from django.utils.translation import gettext_lazy as _
from mailbox_manager import models
class UserMailDomainAccessInline(admin.TabularInline):
"""Inline admin class for mail domain accesses."""
extra = 0
model = models.MailDomainAccess
readonly_fields = ("created_at", "updated_at", "domain", "user")
@admin.register(models.MailDomain)
class MailDomainAdmin(admin.ModelAdmin):
"""Mail domain admin interface declaration."""
@@ -19,6 +27,7 @@ class MailDomainAdmin(admin.ModelAdmin):
)
search_fields = ("name",)
readonly_fields = ["created_at", "slug"]
inlines = (UserMailDomainAccessInline,)
@admin.register(models.MailDomainAccess)
@@ -500,7 +500,8 @@ def test_api_mailboxes__user_unrelated_to_domain():
assert not models.Mailbox.objects.exists()
def test_api_mailboxes__handling_dimail_unexpected_error():
@mock.patch.object(Logger, "error")
def test_api_mailboxes__handling_dimail_unexpected_error(mock_error):
"""
API should raise a clear error when dimail returns an unexpected response.
"""
@@ -542,6 +543,12 @@ def test_api_mailboxes__handling_dimail_unexpected_error():
}
assert not models.Mailbox.objects.exists()
# Check error logger was called
assert mock_error.called
assert mock_error.call_args_list[1][0] == (
'Unexpected response from dimail: 500 {"details": "Internal server error"}',
)
@mock.patch.object(Logger, "error")
@mock.patch.object(Logger, "info")
+6 -2
View File
@@ -120,8 +120,12 @@ class DimailAPIClient:
"""Raise error when encountering an unexpected error in dimail."""
error_content = response.content.decode("utf-8")
logger.error("[DIMAIL] unexpected error : %s", error_content)
raise SystemError(f"Unexpected response from dimail: {error_content}")
logger.error(
"[DIMAIL] unexpected error : %s %s", response.status_code, error_content
)
raise SystemError(
f"Unexpected response from dimail: {response.status_code} {error_content}"
)
def send_new_mailbox_notification(self, recipient, mailbox_data):
"""
+7 -1
View File
@@ -413,6 +413,11 @@ class Base(Configuration):
)
OIDC_TIMEOUT = values.Value(None, environ_name="OIDC_TIMEOUT", environ_prefix=None)
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = values.BooleanValue(
default=True,
environ_name="OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION",
environ_prefix=None,
)
# MAILBOX-PROVISIONING API
WEBMAIL_URL = values.Value(
@@ -482,7 +487,8 @@ class Base(Configuration):
environment=cls.__name__.lower(),
release=get_release(),
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
traces_sample_rate=0.1,
default_integrations=False,
)
with sentry_sdk.configure_scope() as scope:
scope.set_extra("application", "backend")
+11 -11
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "people"
version = "1.2.1"
version = "1.4.1"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,18 +25,18 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.35.29",
"boto3==1.35.44",
"Brotli==1.1.0",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
"django-cors-headers==4.4.0",
"django-cors-headers==4.5.0",
"django-countries==7.6.1",
"django-parler==2.3",
"redis==5.1.0",
"redis==5.1.1",
"django-redis==5.4.0",
"django-storages==1.14.4",
"django-timezone-field>=5.1",
"django==5.1.1",
"django==5.1.2",
"djangorestframework==3.15.2",
"drf_spectacular==0.27.2",
"dockerflow==2024.4.2",
@@ -49,7 +49,7 @@ dependencies = [
"PyJWT==2.9.0",
"joserfc==1.0.0",
"requests==2.32.3",
"sentry-sdk[django]==2.14.0",
"sentry-sdk[django]==2.17.0",
"url-normalize==1.4.3",
"whitenoise==6.7.0",
"mozilla-django-oidc==4.0.1",
@@ -66,9 +66,9 @@ dev = [
"django-extensions==3.2.3",
"drf-spectacular-sidecar==2024.7.1",
"ipdb==0.13.13",
"ipython==8.27.0",
"pyfakefs==5.6.0",
"pylint-django==2.5.5",
"ipython==8.28.0",
"pyfakefs==5.7.1",
"pylint-django==2.6.1",
"pylint==3.3.1",
"pytest-cov==5.0.0",
"pytest-django==4.9.0",
@@ -76,8 +76,8 @@ dev = [
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.3",
"ruff==0.6.8",
"types-requests==2.32.0.20240914",
"ruff==0.7.0",
"types-requests==2.32.0.20241016",
"freezegun==1.5.1",
]
+5
View File
@@ -1,3 +1,5 @@
const path = require('path');
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
@@ -5,6 +7,9 @@ const nextConfig = {
images: {
unoptimized: true,
},
sassOptions: {
includePaths: [path.join(__dirname, 'src')],
},
compiler: {
// Enables the styled-components SWC transform
styledComponents: true,
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-desk",
"version": "1.2.1",
"version": "1.4.1",
"private": true,
"scripts": {
"dev": "next dev",
@@ -29,6 +29,7 @@
"react-hook-form": "7.53.0",
"react-i18next": "15.0.2",
"react-select": "5.8.1",
"sass": "1.80.3",
"styled-components": "6.1.13",
"zod": "3.23.8",
"zustand": "4.5.5"
@@ -21,7 +21,7 @@ describe('Page', () => {
it('checks Page rendering with team feature', () => {
useConfigStore.setState({
config: { FEATURES: { TEAMS: true }, LANGUAGES: [] },
config: { RELEASE: '1.0.0', FEATURES: { TEAMS: true }, LANGUAGES: [] },
});
render(<Page />, { wrapper: AppWrapper });
@@ -31,7 +31,7 @@ describe('Page', () => {
it('checks Page rendering without team feature', () => {
useConfigStore.setState({
config: { FEATURES: { TEAMS: false }, LANGUAGES: [] },
config: { RELEASE: '1.0.0', FEATURES: { TEAMS: false }, LANGUAGES: [] },
});
render(<Page />, { wrapper: AppWrapper });
@@ -0,0 +1,54 @@
import * as React from 'react';
import { ReactNode } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components';
import { Box } from '@/components';
import style from './custom-tabs.module.scss';
type TabsOption = {
ariaLabel?: string;
label: string;
iconName?: string;
id?: string;
content: ReactNode;
};
type Props = {
tabs: TabsOption[];
};
export const CustomTabs = ({ tabs }: Props) => {
return (
<div className={style.customTabsContainer}>
<Tabs>
<TabList>
{tabs.map((tab) => {
const id = tab.id ?? tab.label;
return (
<Tab key={id} aria-label={tab.ariaLabel} id={id}>
<Box $direction="row" $align="center" $gap="5px">
{tab.iconName && (
<span className="material-icons" aria-hidden="true">
{tab.iconName}
</span>
)}
{tab.label}
</Box>
</Tab>
);
})}
</TabList>
{tabs.map((tab) => {
const id = tab.id ?? tab.label;
return (
<TabPanel key={id} id={id}>
{tab.content}
</TabPanel>
);
})}
</Tabs>
</div>
);
};
@@ -0,0 +1,63 @@
.customTabsContainer {
:global {
.react-aria-TabList {
display: flex;
&[data-orientation='horizontal'] {
.react-aria-Tab {
border-bottom: 2px solid var(--c--theme--colors--greyscale-500);
}
}
}
.react-aria-Tab {
padding: 10px;
cursor: pointer;
outline: none;
position: relative;
color: var(--c--theme--colors--greyscale-700);
transition: color 200ms;
--border-color: transparent;
forced-color-adjust: none;
&[data-hovered],
&[data-focused] {
color: var(--c--theme--colors--greyscale-900);
}
&[data-selected] {
border-bottom: 2px solid var(--c--theme--colors--primary-600) !important;
color: var(--c--theme--colors--primary-600);
}
&[data-disabled] {
color: var(--c--theme--colors--greyscale-500);
&[data-selected] {
--border-color: var(--c--theme--colors--greyscale-200);
}
}
&[data-focus-visible]::after {
content: '';
position: absolute;
inset: 4px;
border-radius: 4px;
border: 1px solid var(--c--theme--colors--primary-600);
}
}
.react-aria-TabPanel {
margin-top: 4px;
padding: 10px;
border-radius: 4px;
outline: none;
&[data-focus-visible] {
outline: 2px solid var(--c--theme--colors--primary-600);
}
}
}
}
@@ -17,7 +17,7 @@ jest.mock('next/navigation', () => ({
describe('MainLayout', () => {
it('checks menu rendering with team feature', () => {
useConfigStore.setState({
config: { FEATURES: { TEAMS: true }, LANGUAGES: [] },
config: { RELEASE: '1.0.0', FEATURES: { TEAMS: true }, LANGUAGES: [] },
});
render(<MainLayout />, { wrapper: AppWrapper });
@@ -37,7 +37,7 @@ describe('MainLayout', () => {
it('checks menu rendering without team feature', () => {
useConfigStore.setState({
config: { FEATURES: { TEAMS: false }, LANGUAGES: [] },
config: { RELEASE: '1.0.0', FEATURES: { TEAMS: false }, LANGUAGES: [] },
});
render(<MainLayout />, { wrapper: AppWrapper });
@@ -1,5 +1,6 @@
export interface Config {
LANGUAGES: [string, string][];
RELEASE: string;
FEATURES: {
TEAMS: boolean;
};
@@ -3,6 +3,7 @@ import { Trans, useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { Box, LogoGouv, StyledLink, Text } from '@/components';
import { useConfigStore } from '@/core';
import IconLink from './assets/external-link.svg';
@@ -16,6 +17,7 @@ const BlueStripe = styled.div`
export const Footer = () => {
const { t } = useTranslation();
const { config } = useConfigStore();
return (
<Box $position="relative" as="footer">
@@ -94,7 +96,7 @@ export const Footer = () => {
$padding={{ top: 'tiny' }}
$css="
flex-wrap: wrap;
border-top: 1px solid var(--c--theme--colors--greyscale-200);
border-top: 1px solid var(--c--theme--colors--greyscale-200);
column-gap: 1rem;
row-gap: .5rem;
"
@@ -138,6 +140,7 @@ export const Footer = () => {
</StyledLink>
))}
</Box>
<Text
as="p"
$size="m"
@@ -145,6 +148,12 @@ export const Footer = () => {
$variation="600"
$display="inline"
>
{config?.RELEASE && (
<>
{t(`Version: {{release}}`, { release: config?.RELEASE })} &nbsp;
</>
)}
<Trans>
Unless otherwise stated, all content on this site is under
<StyledLink
@@ -1,11 +1,6 @@
import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components';
import { AccessesGrid } from '@/features/mail-domains/access-management/components/AccessesGrid';
import MailDomainsLogo from '@/features/mail-domains/assets/mail-domains-logo.svg';
import { MailDomain, Role } from '../../domains';
@@ -17,50 +12,6 @@ export const AccessesContent = ({
currentRole: Role;
}) => (
<>
<TopBanner mailDomain={mailDomain} />
<AccessesGrid mailDomain={mailDomain} currentRole={currentRole} />
</>
);
const TopBanner = ({ mailDomain }: { mailDomain: MailDomain }) => {
const router = useRouter();
const { t } = useTranslation();
return (
<Box
$direction="column"
$margin={{ all: 'big', bottom: 'tiny' }}
$gap="1rem"
>
<Box
$direction="row"
$align="center"
$gap="2.25rem"
$justify="space-between"
>
<Box $direction="row" $margin="none" $gap="0.5rem">
<MailDomainsLogo aria-hidden="true" />
<Text $margin="none" as="h3" $size="h3">
{mailDomain?.name}
</Text>
</Box>
</Box>
<Box $direction="row" $justify="flex-end">
<Box $display="flex" $direction="row" $gap="8rem">
{mailDomain?.abilities?.manage_accesses && (
<Button
color="tertiary"
aria-label={t('Manage {{name}} domain mailboxes', {
name: mailDomain?.name,
})}
onClick={() => router.push(`/mail-domains/${mailDomain.slug}/`)}
>
{t('Manage mailboxes')}
</Button>
)}
</Box>
</Box>
</Box>
);
};
@@ -106,8 +106,6 @@ export const AccessesGrid = ({
return (
<Card
$padding={{ bottom: 'small' }}
$margin={{ all: 'big', top: 'none' }}
$overflow="auto"
$css={`
& .c__pagination__goto {
@@ -3,8 +3,11 @@ import { useTranslation } from 'react-i18next';
import { Role } from '../../domains';
const ORDERED_ROLES = [Role.VIEWER, Role.ADMIN, Role.OWNER];
interface ChooseRoleProps {
availableRoles: Role[];
roleAccess: Role;
currentRole: Role;
disabled: boolean;
setRole: (role: Role) => void;
@@ -12,55 +15,44 @@ interface ChooseRoleProps {
export const ChooseRole = ({
availableRoles,
roleAccess,
disabled,
currentRole,
setRole,
}: ChooseRoleProps) => {
const { t } = useTranslation();
const rolesToDisplay = Array.from(new Set([currentRole, ...availableRoles]));
const rolesToDisplay: Role[] = Array.from(
new Set([roleAccess, ...availableRoles]),
);
const translations = {
[Role.VIEWER]: t('Viewer'),
[Role.ADMIN]: t('Administrator'),
[Role.OWNER]: t('Owner'),
};
return (
<RadioGroup>
{rolesToDisplay?.map((role) => {
switch (role) {
case Role.VIEWER:
return (
<Radio
key={Role.VIEWER}
label={t('Viewer')}
value={Role.VIEWER}
name="role"
onChange={(evt) => setRole(evt.target.value as Role)}
defaultChecked={currentRole === Role.VIEWER}
disabled={disabled}
/>
);
case Role.ADMIN:
return (
<Radio
key={Role.ADMIN}
label={t('Administrator')}
value={Role.ADMIN}
name="role"
onChange={(evt) => setRole(evt.target.value as Role)}
defaultChecked={currentRole === Role.ADMIN}
disabled={disabled}
/>
);
case Role.OWNER:
return (
<Radio
key={Role.OWNER}
label={t('Owner')}
value={Role.OWNER}
name="role"
onChange={(evt) => setRole(evt.target.value as Role)}
defaultChecked={currentRole === Role.OWNER}
disabled={disabled || currentRole !== Role.OWNER}
/>
);
}
})}
{ORDERED_ROLES.filter((role) => rolesToDisplay.includes(role)).map(
(role) => {
let disableRadio = disabled;
if (role === Role.OWNER) {
disableRadio = disableRadio || currentRole !== Role.OWNER;
}
return (
<Radio
key={role}
label={translations[role]}
value={role}
name="role"
onChange={(evt) => setRole(evt.target.value as Role)}
defaultChecked={roleAccess === role}
disabled={disableRadio}
/>
);
},
)}
</RadioGroup>
);
};
@@ -7,12 +7,11 @@ import {
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text, TextErrors } from '@/components';
import { Box, TextErrors } from '@/components';
import { Modal } from '@/components/Modal';
import { useUpdateMailDomainAccess } from '@/features/mail-domains/access-management';
import { Role } from '../../domains';
import { useWhoAmI } from '../hooks/useWhoAmI';
import { Access } from '../types';
import { ChooseRole } from './ChooseRole';
@@ -46,13 +45,10 @@ export const ModalRole = ({
onClose();
},
});
const { isLastOwner, isOtherOwner } = useWhoAmI(access);
const isNotAllowed = isOtherOwner || isLastOwner;
return (
<Modal
isOpen
isOpen={true}
leftActions={
<Button
color="secondary"
@@ -77,7 +73,7 @@ export const ModalRole = ({
accessId: access.id,
});
}}
disabled={isNotAllowed || isPending}
disabled={isPending}
>
{t('Validate')}
</Button>
@@ -93,28 +89,11 @@ export const ModalRole = ({
/>
)}
{(isLastOwner || isOtherOwner) && (
<Text
$theme="warning"
$direction="row"
$align="center"
$gap="0.5rem"
$margin={{ bottom: 'tiny' }}
$justify="center"
>
<span className="material-icons">warning</span>
{isLastOwner &&
t(
'You are the sole owner of this domain. Make another member the domain owner, before you can change your own role.',
)}
{isOtherOwner && t('You cannot update the role of other owner.')}
</Text>
)}
<ChooseRole
roleAccess={access.role}
availableRoles={access.can_set_role_to}
currentRole={currentRole}
disabled={isNotAllowed}
disabled={false}
setRole={setLocalRole}
/>
</Box>
@@ -1,8 +1,6 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { useRouter } from 'next/navigation';
import { AccessesGrid } from '@/features/mail-domains/access-management';
import { AppWrapper } from '@/tests/utils';
import { MailDomain, Role } from '../../../domains';
@@ -61,58 +59,9 @@ describe('AccessesContent', () => {
});
});
it('renders the top banner and accesses grid correctly', () => {
it('renders the accesses grid correctly', () => {
renderAccessesContent();
expect(screen.getByText(mockMailDomain.name)).toBeInTheDocument();
expect(screen.getByTestId('mail-domains-logo')).toBeInTheDocument();
expect(screen.getByText('Mock AccessesGrid')).toBeInTheDocument();
});
it('renders the "Manage mailboxes" button when the user has access', () => {
renderAccessesContent();
const manageMailboxesButton = screen.getByRole('button', {
name: /Manage example.com domain mailboxes/,
});
expect(manageMailboxesButton).toBeInTheDocument();
expect(AccessesGrid).toHaveBeenCalledWith(
{ currentRole: Role.ADMIN, mailDomain: mockMailDomain },
{}, // adding this empty object is necessary to load jest context and that AccessesGrid is a mock
);
});
it('does not render the "Manage mailboxes" button if the user lacks manage_accesses ability', () => {
const mailDomainWithoutAccess = {
...mockMailDomain,
abilities: {
...mockMailDomain.abilities,
manage_accesses: false,
},
};
renderAccessesContent(Role.ADMIN, mailDomainWithoutAccess);
expect(
screen.queryByRole('button', {
name: /Manage mailboxes/i,
}),
).not.toBeInTheDocument();
});
it('navigates to the mailboxes management page when "Manage mailboxes" is clicked', async () => {
renderAccessesContent();
const manageMailboxesButton = screen.getByRole('button', {
name: /Manage example.com domain mailboxes/,
});
await userEvent.click(manageMailboxesButton);
await waitFor(() => {
expect(mockRouterPush).toHaveBeenCalledWith(`/mail-domains/example-com/`);
});
});
});
@@ -15,6 +15,7 @@ describe('ChooseRole', () => {
) => {
const defaultProps = {
availableRoles: [Role.VIEWER, Role.ADMIN],
roleAccess: Role.ADMIN,
currentRole: Role.ADMIN,
disabled: false,
setRole: mockSetRole,
@@ -28,13 +29,22 @@ describe('ChooseRole', () => {
jest.clearAllMocks();
});
it('renders available roles correctly', () => {
it('renders available roles correctly when we are Administrator', () => {
renderChooseRole();
expect(screen.getByLabelText('Viewer')).toBeInTheDocument();
expect(screen.getByLabelText('Administrator')).toBeInTheDocument();
});
it('renders available roles correctly when we are owner', () => {
renderChooseRole({
currentRole: Role.OWNER,
roleAccess: Role.OWNER,
});
expect(screen.getByLabelText('Viewer')).toBeInTheDocument();
expect(screen.getByLabelText('Administrator')).toBeInTheDocument();
expect(screen.getByLabelText('Owner')).toBeInTheDocument();
});
it('sets default role checked correctly', () => {
renderChooseRole({ currentRole: Role.ADMIN });
@@ -91,6 +101,7 @@ describe('ChooseRole', () => {
it('renders and checks owner role correctly when currentRole is owner', () => {
renderChooseRole({
currentRole: Role.OWNER,
roleAccess: Role.OWNER,
availableRoles: [Role.OWNER, Role.VIEWER, Role.ADMIN],
});
@@ -115,26 +115,6 @@ describe('ModalRole', () => {
);
});
it('disables the Validate button if the user is the last owner', () => {
renderModalRole(true, false); // isLastOwner = true, isOtherOwner = false
const validateButton = screen.getByRole('button', { name: /Validate/i });
expect(validateButton).toBeDisabled();
expect(
screen.getByText(/You are the sole owner of this domain/i),
).toBeInTheDocument();
});
it('disables the Validate button if the user is another owner', () => {
renderModalRole(false, true); // isLastOwner = false, isOtherOwner = true
const validateButton = screen.getByRole('button', { name: /Validate/i });
expect(validateButton).toBeDisabled();
expect(
screen.getByText(/You cannot update the role of other owner/i),
).toBeInTheDocument();
});
it('shows error message when update fails', async () => {
fetchMock.patch(`end:mail-domains/domain-slug/accesses/1-1-1-1-1-1/`, {
status: 400,
@@ -0,0 +1,68 @@
import * as React from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components';
import { CustomTabs } from '@/components/tabs/CustomTabs';
import { AccessesContent } from '@/features/mail-domains/access-management';
import MailDomainsLogo from '@/features/mail-domains/assets/mail-domains-logo.svg';
import { MailDomain, Role } from '@/features/mail-domains/domains';
import { MailDomainsContent } from '@/features/mail-domains/mailboxes';
type Props = {
mailDomain: MailDomain;
};
export const MailDomainView = ({ mailDomain }: Props) => {
const { t } = useTranslation();
const currentRole = mailDomain.abilities.delete
? Role.OWNER
: mailDomain.abilities.manage_accesses
? Role.ADMIN
: Role.VIEWER;
const tabs = useMemo(() => {
return [
{
ariaLabel: t('Go to mailbox management'),
id: 'mails',
iconName: 'mail',
label: t('Mailbox management'),
content: <MailDomainsContent mailDomain={mailDomain} />,
},
{
ariaLabel: t('Go to accesses management'),
id: 'accesses',
iconName: 'people',
label: t('Access management'),
content: (
<AccessesContent mailDomain={mailDomain} currentRole={currentRole} />
),
},
];
}, [t, currentRole, mailDomain]);
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"
>
<MailDomainsLogo aria-hidden="true" />
<Text $margin="none" as="h3" $size="h3">
{mailDomain?.name}
</Text>
</Box>
</Box>
<CustomTabs tabs={tabs} />
</Box>
);
};
@@ -16,6 +16,7 @@ export function MailDomainsLayout({ children }: PropsWithChildren) {
<Box
$background={colorsTokens()['primary-bg']}
$width="100%"
$overflow="auto"
$height="inherit"
>
{children}
@@ -1,4 +1,5 @@
import { UUID } from 'crypto';
export interface MailDomain {
id: UUID;
name: string;
@@ -9,14 +9,12 @@ import {
VariantType,
usePagination,
} from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Card, Text, TextErrors, TextStyled } from '@/components';
import { ModalCreateMailbox } from '@/features/mail-domains/mailboxes';
import { default as MailDomainsLogo } from '../../assets/mail-domains-logo.svg';
import { PAGE_SIZE } from '../../conf';
import { MailDomain } from '../../domains/types';
import { useMailboxes } from '../api/useMailboxes';
@@ -99,12 +97,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
showMailBoxCreationForm={setIsCreateMailboxFormVisible}
/>
<Card
$padding={{ bottom: 'small' }}
$margin={{ all: 'big', top: 'none' }}
$overflow="auto"
aria-label={t('Mailboxes list card')}
>
<Card $overflow="auto" aria-label={t('Mailboxes list card')}>
{error && <TextErrors causes={error.cause} />}
<DataGrid
@@ -153,47 +146,19 @@ const TopBanner = ({
mailDomain: MailDomain;
showMailBoxCreationForm: (value: boolean) => void;
}) => {
const router = useRouter();
const { t } = useTranslation();
return (
<Box
$direction="column"
$margin={{ all: 'big', bottom: 'tiny' }}
$gap="1rem"
>
<Box $direction="column" $gap="1rem">
<AlertStatus status={mailDomain.status} />
<Box
$direction="row"
$justify="flex-end"
$margin={{ bottom: 'small' }}
$align="center"
$gap="2.25rem"
$justify="space-between"
>
<Box $direction="row" $margin="none" $gap="0.5rem">
<MailDomainsLogo aria-hidden="true" />
<Text $margin="none" as="h3" $size="h3">
{mailDomain?.name}
</Text>
</Box>
</Box>
<Box $direction="row" $justify="space-between">
<AlertStatus status={mailDomain.status} />
</Box>
<Box $direction="row" $justify="flex-end">
<Box $display="flex" $direction="row" $gap="0.5rem">
{mailDomain?.abilities?.manage_accesses && (
<Button
color="tertiary"
aria-label={t('Manage {{name}} domain members', {
name: mailDomain?.name,
})}
onClick={() =>
router.push(`/mail-domains/${mailDomain.slug}/accesses/`)
}
>
{t('Manage accesses')}
</Button>
)}
<Box $display="flex" $direction="row">
{mailDomain?.abilities.post && (
<Button
aria-label={t('Create a mailbox in {{name}} domain', {
@@ -187,25 +187,6 @@ describe('MailDomainsContent', () => {
});
});
it('redirects to accesses management page when button is clicked by granted user', async () => {
fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', {
count: 0,
results: [],
});
render(<MailDomainsContent mailDomain={mockMailDomain} />, {
wrapper: AppWrapper,
});
await waitFor(async () => {
await userEvent.click(screen.getByText('Manage accesses'));
});
expect(mockPush).toHaveBeenCalledWith(
'/mail-domains/example-com/accesses/',
);
});
it('displays the correct alert based on mail domain status', async () => {
fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', {
count: 0,
@@ -1,5 +1,4 @@
{
"de": { "translation": {} },
"en": {
"translation": {
"{{count}} member_many": "{{count}} members",
@@ -11,6 +10,7 @@
"translation": {
"0 group to display.": "0 groupe à afficher.",
"Access icon": "Icône d'accès",
"Access management": "Gestion des rôles",
"Accesses list card": "Carte de la liste des accès",
"Accessibility statement": "Déclaration d'accessibilité",
"Accessibility: non-compliant": "Accessibilité : non conforme",
@@ -68,6 +68,8 @@
"First name": "Prénom",
"Freedom Equality Fraternity Logo": "Logo Liberté Égalité Fraternité",
"French Interministerial Directorate for Digital Affairs (DINUM), 20 avenue de Ségur 75007 Paris.": "Direction interministérielle des affaires numériques (DINUM), 20 avenue de Segur 75007 Paris.",
"Go to accesses management": "Aller à la gestion des rôles",
"Go to mailbox management": "Aller à la gestion des mails",
"Group details": "Détails du groupe",
"Group members": "Membres du groupe",
"Groups": "Groupes",
@@ -94,12 +96,9 @@
"Mail domains panel": "Panel des domaines de messagerie",
"Mailbox created!": "Boîte mail créée !",
"Mailbox creation form": "Formulaire de création de boite mail",
"Mailbox management": "Gestion des boîtes mails",
"Mailboxes list": "Liste des boîtes mail",
"Mailboxes list card": "Carte liste des boîtes mails",
"Manage accesses": "Gérer les accès",
"Manage mailboxes": "Gérer les boîtes mails",
"Manage {{name}} domain mailboxes": "Gérer les boîtes mails du domaine {{name}}",
"Manage {{name}} domain members": "Gérer les membres du domaine {{name}}",
"Marianne Logo": "Logo Marianne",
"Member": "Membre",
"Member icon": "Icône de membre",
@@ -155,6 +154,7 @@
"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 sengage à rendre son service accessible, conformément à larticle 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 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",
"The role has been updated": "Le rôle a bien été mis à jour",
"The team has been removed.": "Le groupe a été supprimé.",
@@ -177,11 +177,11 @@
"Update the team": "Mettre à jour le groupe",
"Validate": "Valider",
"Validate the modification": "Valider la modification",
"Version: {{release}}": "Version : {{release}}",
"Viewer": "Lecteur",
"We simply comply with the law, which states that certain audience measurement tools, properly configured to respect privacy, are exempt from prior authorization.": "Nous nous conformons simplement à la loi, qui stipule que certains outils de mesure daudience, correctement configurés pour respecter la vie privée, sont exemptés de toute autorisation préalable.",
"You are the last owner, you cannot be removed from your domain.": "Vous êtes le dernier propriétaire, vous ne pouvez pas être retiré de votre domaine.",
"You are the last owner, you cannot be removed from your team.": "Vous êtes le dernier propriétaire, vous ne pouvez pas être retiré de votre groupe.",
"You are the sole owner of this domain. Make another member the domain owner, before you can change your own role.": "Vous êtes le seul propriétaire de ce domaine. Faites d'un autre membre le propriétaire du domaine avant de modifier votre rôle.",
"You are the sole owner of this group. Make another member the group owner, before you can change your own role.": "Vous êtes lunique propriétaire de ce groupe. Désignez un autre membre comme propriétaire du groupe, avant de pouvoir modifier votre propre rôle.",
"You can oppose the tracking of your browsing on this website.": "Vous pouvez vous opposer au suivi de votre navigation sur ce site.",
"You can:": "Vous pouvez :",
@@ -190,7 +190,6 @@
"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",
"Your request to create a mailbox cannot be completed due to incorrect settings on our server. Please contact our support team to resolve the problem: suiteterritoriale@anct.gouv.fr": "Votre demande de création de boîte mail ne peut pas être complétée en raison de paramètres incorrects sur notre serveur. Veuillez contacter notre équipe support pour résoudre le problème : suiteterritoriale@anct.gouv.fr",
"[disabled]": "[désactivé]",
"[enabled]": "[actif]",
"[failed]": "[erroné]",
@@ -1,7 +1,7 @@
import { Loader } from '@openfun/cunningham-react';
import { useRouter as useNavigate } from 'next/navigation';
import { useRouter } from 'next/router';
import { ReactElement } from 'react';
import React, { ReactElement } from 'react';
import { Box } from '@/components';
import { TextErrors } from '@/components/TextErrors';
@@ -9,7 +9,7 @@ import {
MailDomainsLayout,
useMailDomain,
} from '@/features/mail-domains/domains';
import { MailDomainsContent } from '@/features/mail-domains/mailboxes';
import { MailDomainView } from '@/features/mail-domains/domains/components/MailDomainView';
import { NextPageWithLayout } from '@/types/next';
const MailboxesPage: NextPageWithLayout = () => {
@@ -45,9 +45,13 @@ const MailboxesPage: NextPageWithLayout = () => {
<Loader />
</Box>
);
} else {
return mailDomain ? <MailDomainsContent mailDomain={mailDomain} /> : null;
}
if (!mailDomain) {
return null;
}
return <MailDomainView mailDomain={mailDomain} />;
};
MailboxesPage.getLayout = function getLayout(page: ReactElement) {
@@ -23,6 +23,7 @@ test.describe('Config', () => {
['fr-fr', 'French'],
],
FEATURES: { TEAMS: true },
RELEASE: 'NA',
});
});
@@ -87,4 +87,13 @@ test.describe('Footer', () => {
await expect(page).toHaveURL(url);
});
}
test('check if the app version is visible', async ({ page }) => {
const footer = page.locator('footer').first();
await expect(
footer.getByText(
'Version: NA • Unless otherwise stated, all content on this site is under',
),
).toBeVisible();
});
});
@@ -211,6 +211,19 @@ test.describe('Mail domain', () => {
},
];
test('checks if all tabs are visible', async ({ page }) => {
await interceptCommonApiCalls(page, mailDomainsFixtures);
await clickOnMailDomainsNavButton(page);
await assertMailDomainUpperElementsAreVisible(page);
await expect(
page.getByLabel('Go to accesses management'),
).toBeVisible();
await expect(page.getByLabel('Go to mailbox management')).toBeVisible();
});
test('checks all the elements are visible when domain exist but contains no mailboxes', async ({
page,
}) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "1.2.1",
"version": "1.4.1",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "people",
"version": "1.2.1",
"version": "1.4.1",
"private": true,
"workspaces": {
"packages": [
@@ -1,6 +1,6 @@
{
"name": "eslint-config-people",
"version": "1.2.1",
"version": "1.4.1",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "1.2.1",
"version": "1.4.1",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:desk",
+129 -4
View File
@@ -1880,6 +1880,89 @@
figlet "1.7.0"
ts-node "10.9.2"
"@parcel/watcher-android-arm64@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84"
integrity sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==
"@parcel/watcher-darwin-arm64@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz#c817c7a3b4f3a79c1535bfe54a1c2818d9ffdc34"
integrity sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==
"@parcel/watcher-darwin-x64@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz#1a3f69d9323eae4f1c61a5f480a59c478d2cb020"
integrity sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==
"@parcel/watcher-freebsd-x64@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz#0d67fef1609f90ba6a8a662bc76a55fc93706fc8"
integrity sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==
"@parcel/watcher-linux-arm-glibc@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz#ce5b340da5829b8e546bd00f752ae5292e1c702d"
integrity sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==
"@parcel/watcher-linux-arm64-glibc@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz#6d7c00dde6d40608f9554e73998db11b2b1ff7c7"
integrity sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==
"@parcel/watcher-linux-arm64-musl@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz#bd39bc71015f08a4a31a47cd89c236b9d6a7f635"
integrity sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==
"@parcel/watcher-linux-x64-glibc@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz#0ce29966b082fb6cdd3de44f2f74057eef2c9e39"
integrity sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==
"@parcel/watcher-linux-x64-musl@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz#d2ebbf60e407170bb647cd6e447f4f2bab19ad16"
integrity sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==
"@parcel/watcher-win32-arm64@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz#eb4deef37e80f0b5e2f215dd6d7a6d40a85f8adc"
integrity sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==
"@parcel/watcher-win32-ia32@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz#94fbd4b497be39fd5c8c71ba05436927842c9df7"
integrity sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==
"@parcel/watcher-win32-x64@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz#4bf920912f67cae5f2d264f58df81abfea68dadf"
integrity sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==
"@parcel/watcher@^2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.4.1.tgz#a50275151a1bb110879c6123589dba90c19f1bf8"
integrity sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==
dependencies:
detect-libc "^1.0.3"
is-glob "^4.0.3"
micromatch "^4.0.5"
node-addon-api "^7.0.0"
optionalDependencies:
"@parcel/watcher-android-arm64" "2.4.1"
"@parcel/watcher-darwin-arm64" "2.4.1"
"@parcel/watcher-darwin-x64" "2.4.1"
"@parcel/watcher-freebsd-x64" "2.4.1"
"@parcel/watcher-linux-arm-glibc" "2.4.1"
"@parcel/watcher-linux-arm64-glibc" "2.4.1"
"@parcel/watcher-linux-arm64-musl" "2.4.1"
"@parcel/watcher-linux-x64-glibc" "2.4.1"
"@parcel/watcher-linux-x64-musl" "2.4.1"
"@parcel/watcher-win32-arm64" "2.4.1"
"@parcel/watcher-win32-ia32" "2.4.1"
"@parcel/watcher-win32-x64" "2.4.1"
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
@@ -3409,7 +3492,7 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
"@types/node@*", "@types/node@20.16.10":
"@types/node@*":
version "20.16.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.16.10.tgz#0cc3fdd3daf114a4776f54ba19726a01c907ef71"
integrity sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==
@@ -3426,7 +3509,7 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
"@types/react-dom@*", "@types/react-dom@18.3.0":
"@types/react-dom@*":
version "18.3.0"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
@@ -4363,6 +4446,13 @@ cheerio@^1.0.0:
undici "^6.19.5"
whatwg-mimetype "^4.0.0"
chokidar@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41"
integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==
dependencies:
readdirp "^4.0.1"
chromatic@11.7.1:
version "11.7.1"
resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.7.1.tgz#9de59dd9d0e2a847627bccd959f05881335b524e"
@@ -4797,6 +4887,11 @@ dequal@^2.0.2, dequal@^2.0.3:
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
@@ -6171,6 +6266,11 @@ ignore@^5.2.0, ignore@^5.3.1, ignore@^5.3.2:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
immutable@^4.0.0:
version "4.3.7"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
import-fresh@^3.2.1, import-fresh@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@@ -7278,7 +7378,7 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4, micromatch@^4.0.8:
micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
@@ -7400,6 +7500,11 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
node-addon-api@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
node-fetch@2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
@@ -8154,6 +8259,11 @@ readable-stream@~2.3.6:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readdirp@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@@ -8375,6 +8485,16 @@ safe-regex-test@^1.0.3:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass@^1.80.3:
version "1.80.3"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.3.tgz#3f63dd527647d2b3de35f36acb971bda80517423"
integrity sha512-ptDWyVmDMVielpz/oWy3YP3nfs7LpJTHIJZboMVs8GEC9eUmtZTZhMHlTW98wY4aEorDfjN38+Wr/XjskFWcfA==
dependencies:
"@parcel/watcher" "^2.4.1"
chokidar "^4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
saxes@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
@@ -8492,6 +8612,11 @@ sort-keys@^5.0.0:
dependencies:
is-plain-obj "^4.0.0"
"source-map-js@>=0.6.2 <2.0.0":
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==
source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
@@ -9134,7 +9259,7 @@ typed-array-length@^1.0.6:
is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0"
typescript@*, typescript@5.6.2, typescript@^5.0.4:
typescript@*, typescript@^5.0.4:
version "5.6.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
@@ -1,7 +1,7 @@
image:
repository: lasuite/people-backend
pullPolicy: Always
tag: "v1.2.1"
tag: "v1.4.1"
backend:
migrateJobAnnotations:
@@ -113,7 +113,7 @@ frontend:
image:
repository: lasuite/people-frontend
pullPolicy: Always
tag: "v1.0.1"
tag: "v1.4.1"
ingress:
enabled: true
@@ -1,7 +1,7 @@
image:
repository: lasuite/people-backend
pullPolicy: Always
tag: "v1.2.1"
tag: "v1.4.1"
backend:
migrateJobAnnotations:
@@ -113,7 +113,7 @@ frontend:
image:
repository: lasuite/people-frontend
pullPolicy: Always
tag: "v1.0.1"
tag: "v1.4.1"
ingress:
enabled: true
+20 -23
View File
@@ -3,39 +3,36 @@
<mj-body mj-class="bg--blue-100">
<mj-wrapper css-class="wrapper" padding="0 40px 40px 40px">
<mj-section background-url="{% base64_static 'images/mail-header-background.png' %}" background-size="cover" background-repeat="no-repeat" background-position="0 -30px">
<mj-section>
<mj-column>
<mj-image align="center" src="{% base64_static 'images/logo-suite-numerique.png' %}" width="250px" align="left" alt="{% trans 'La Suite Numérique' %}" />
<mj-image align="center" src="{% base64_static 'images/messagerie.png' %}" width="60px" height="60px" alt="{% trans 'La Messagerie' %}" />
</mj-column>
</mj-section>
<mj-section mj-class="bg--white-100" padding="30px 20px 60px 20px">
<mj-section mj-class="bg--white-100" padding="0px 20px">
<mj-column>
<mj-text font-size="14px">
<p>{{ title }}</p>
</mj-text>
<!-- Welcome Message -->
<mj-text>
<h1>{% trans "Your new mailbox is ready !" %}</h1>
</mj-text>
<mj-divider border-width="1px" border-style="solid" border-color="#DDDDDD" width="30%" align="left"/>
<mj-image src="{% base64_static 'images/logo.png' %}" width="157px" align="left" alt="{% trans 'Logo' %}" />
<mj-text font-size="30px"><b>{% trans "Welcome to La Messagerie" %}</b></mj-text>
<mj-text>{% trans "La Messagerie is the email solution of La Suite." %}</mj-text>
<!-- Main Message -->
<mj-text>{% trans "Here are your credentials for your new mailbox ! " %}</mj-text>
<mj-text>{% trans "Email address : "%}{{ mailbox_data.email }}</mj-text>
<mj-text>{% trans "Temporary password : "%}{{ mailbox_data.password }}</mj-text>
<mj-text>{% trans "You can access your mails on " %}<a href="//{{ webmail_url }}">{{ webmail_url }}</a>.</mj-text>
<mj-button href="//{{ site.domain }}" background-color="#000091" color="white" padding-bottom="30px">
{% trans "Visit La Régie" %}
<mj-text>{% trans "Your mailbox has been created." %}</mj-text>
<mj-text>{% trans "Please find below your login info: " %}</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#f3f2fe" padding="0px 20px">
<mj-column>
<mj-text>{% trans "Email address: "%}<b>{{ mailbox_data.email }}</b></mj-text>
<mj-text>{% trans "Temporary password (to be modify on first login): "%}<b>{{ mailbox_data.password }}</b></mj-text>
</mj-column>
</mj-section>
<mj-section padding="0px 20px">
<mj-column>
<mj-button href="//{{ webmail_url }}" background-color="#000091" color="white" padding="30px 0px">
{% trans "Go to La Messagerie" %}
</mj-button>
<!-- Signature -->
<mj-text>
<p>{% trans "Sincerely," %}</p>
<p>{% trans "The La Suite Numérique Team" %}</p>
<p>{% trans "La Suite Team" %}</p>
</mj-text>
</mj-column>
</mj-section>