Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38cfaff81f | |||
| 80ac55b6e2 | |||
| 230ed75d8d | |||
| 5ac3454c5a | |||
| bbac34b62a | |||
| 38508c272e | |||
| 10a81f14e6 | |||
| a08689a64d | |||
| 30229e11f9 | |||
| d11d1f3bd0 | |||
| 6a22169dae | |||
| 6e7a6e9d51 | |||
| 95fb476041 | |||
| 6b4ea1a2e7 | |||
| be55d6ea09 | |||
| 9c9216bb51 | |||
| 017f52a0dc | |||
| d635c484ae | |||
| 0e48bc0f90 | |||
| f243a2423f | |||
| 763eb254a8 | |||
| 9b613e63a9 | |||
| 6b7f00651f | |||
| fe249c5b6f | |||
| 579657afa4 |
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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.
@@ -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,"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })} •
|
||||
</>
|
||||
)}
|
||||
|
||||
<Trans>
|
||||
Unless otherwise stated, all content on this site is under
|
||||
<StyledLink
|
||||
|
||||
-49
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
-2
@@ -106,8 +106,6 @@ export const AccessesGrid = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
$padding={{ bottom: 'small' }}
|
||||
$margin={{ all: 'big', top: 'none' }}
|
||||
$overflow="auto"
|
||||
$css={`
|
||||
& .c__pagination__goto {
|
||||
|
||||
+33
-41
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-26
@@ -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>
|
||||
|
||||
+2
-53
@@ -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/`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+13
-2
@@ -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],
|
||||
});
|
||||
|
||||
|
||||
-20
@@ -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,
|
||||
|
||||
+68
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
@@ -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;
|
||||
|
||||
+7
-42
@@ -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', {
|
||||
|
||||
-19
@@ -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 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 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 d’audience, 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 l’unique 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,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "1.2.1",
|
||||
"version": "1.4.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user