mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-07 15:43:01 +02:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53be6de5f8 | ||
|
|
4ff90abdee | ||
|
|
544dd00c16 | ||
|
|
a3cd4c51ea | ||
|
|
7e1eed3abd | ||
|
|
8bee476b5b | ||
|
|
e86919fb9a | ||
|
|
a5b9169eb6 | ||
|
|
c0dfb4b6b3 | ||
|
|
be051ad7d2 | ||
|
|
a4452784e1 | ||
|
|
2929e98260 | ||
|
|
a1914c6259 | ||
|
|
c882f1386c | ||
|
|
c02f19a2cd | ||
|
|
34a208a80d | ||
|
|
6976bb7c78 | ||
|
|
621393165f | ||
|
|
3e9b530985 | ||
|
|
54f9b3963e | ||
|
|
710bbf512c | ||
|
|
747ca70186 | ||
|
|
9374495fda | ||
|
|
ef7cc67387 | ||
|
|
a8529e434a | ||
|
|
f8203a1766 | ||
|
|
ce8b98e256 | ||
|
|
4243519eee | ||
|
|
1abf529891 | ||
|
|
69ca4af539 |
2
.github/workflows/docker-hub.yml
vendored
2
.github/workflows/docker-hub.yml
vendored
@@ -134,5 +134,5 @@ jobs:
|
||||
name: Call argocd github webhook
|
||||
run: |
|
||||
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/${{ secrets.DEPLOYMENT_REPO_URL }}"}}'
|
||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}" | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||
sig=$(echo -n ${data} | openssl dgst -sha256 -hmac "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}" | awk '{print "X-Hub-Signature-256: sha256="$2}')
|
||||
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" ${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -8,6 +8,23 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.0.0] - 2025-03-28
|
||||
|
||||
## Added
|
||||
|
||||
- 📄(legal) Require contributors to sign a DCO #779
|
||||
|
||||
## Changed
|
||||
|
||||
- ♻️(frontend) Integrate UI kit #783
|
||||
- 🏗️(y-provider) manage auth in y-provider app #804
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(backend) compute ancestor_links in get_abilities if needed #725
|
||||
- 🔒️(back) restrict access to document accesses #801
|
||||
|
||||
|
||||
## [2.6.0] - 2025-03-21
|
||||
|
||||
## Added
|
||||
@@ -124,6 +141,8 @@ and this project adheres to
|
||||
|
||||
## Added
|
||||
|
||||
- ✨(backend) add duplicate action to the document API endpoint
|
||||
- ⚗️(backend) add util to extract text from base64 yjs document
|
||||
- ✨(backend) add soft delete and restore API endpoints to documents #516
|
||||
- ✨(backend) allow organizing documents in a tree structure #516
|
||||
- ✨(backend) add "excerpt" field to document list serializer #516
|
||||
@@ -487,8 +506,9 @@ and this project adheres to
|
||||
- ✨(frontend) Coming Soon page (#67)
|
||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.6.0...main
|
||||
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.6.0
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.0.0...main
|
||||
[v3.0.0]: https://github.com/numerique-gouv/impress/releases/v3.0.0
|
||||
[v2.6.0]: https://github.com/numerique-gouv/impress/releases/v2.6.0
|
||||
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.5.0
|
||||
[v2.4.0]: https://github.com/numerique-gouv/impress/releases/v2.4.0
|
||||
[v2.3.0]: https://github.com/numerique-gouv/impress/releases/v2.3.0
|
||||
|
||||
@@ -4,6 +4,8 @@ Thank you for taking the time to contribute! Please follow these guidelines to e
|
||||
|
||||
To get started with the project, please refer to the [README.md](https://github.com/suitenumerique/docs/blob/main/README.md) for detailed instructions.
|
||||
|
||||
Contributors are required to sign off their commits with `git commit --sign-off`: this confirms that they have read and accepted the [Developer's Certificate of Origin 1.1](https://developercertificate.org/).
|
||||
|
||||
Please also check out our [dev handbook](https://suitenumerique.gitbook.io/handbook) to learn our best practices.
|
||||
|
||||
## Help us with translations
|
||||
|
||||
@@ -15,6 +15,13 @@ FROM base AS back-builder
|
||||
|
||||
WORKDIR /builder
|
||||
|
||||
# Install Rust and Cargo using Alpine's package manager
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
libffi-dev \
|
||||
rust \
|
||||
cargo
|
||||
|
||||
# Copy required python dependencies
|
||||
COPY ./src/backend /builder
|
||||
|
||||
|
||||
12
UPGRADE.md
12
UPGRADE.md
@@ -16,6 +16,18 @@ the following command inside your docker container:
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.0.0] - 2025-03-28
|
||||
|
||||
We are not using the nginx auth request anymore to access the collaboration server (`yProvider`)
|
||||
The authentication is now managed directly from the yProvider server.
|
||||
You must remove the annotation `nginx.ingress.kubernetes.io/auth-url` from the `ingressCollaborationWS`.
|
||||
|
||||
This means as well that the yProvider server must be able to access the Django server.
|
||||
To do so, you must set the `COLLABORATION_BACKEND_BASE_URL` environment variable to the `yProvider`
|
||||
service.
|
||||
|
||||
## [2.2.0] - 2025-02-10
|
||||
|
||||
- AI features are now limited to users who are authenticated. Before this release, even anonymous
|
||||
users who gained editor access on a document with link reach used to get AI feature.
|
||||
IF you want anonymous users to keep access on AI features, you must now define the
|
||||
|
||||
@@ -39,6 +39,9 @@ docker_build(
|
||||
]
|
||||
)
|
||||
|
||||
k8s_resource('impress-docs-backend-migrate', resource_deps=['postgres-postgresql'])
|
||||
k8s_resource('impress-docs-backend-createsuperuser', resource_deps=['impress-docs-backend-migrate'])
|
||||
k8s_resource('impress-docs-backend', resource_deps=['impress-docs-backend-migrate'])
|
||||
k8s_yaml(local('cd ../src/helm && helmfile -n impress -e dev template .'))
|
||||
|
||||
migration = '''
|
||||
|
||||
@@ -185,11 +185,15 @@ services:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/servers/y-provider/Dockerfile
|
||||
target: y-provider
|
||||
command: ["yarn", "workspace", "server-y-provider", "run", "dev"]
|
||||
working_dir: /app/frontend
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
ports:
|
||||
- "4444:4444"
|
||||
volumes:
|
||||
- ./src/frontend/:/app/frontend
|
||||
|
||||
kc_postgresql:
|
||||
image: postgres:14.3
|
||||
|
||||
@@ -4,54 +4,6 @@ server {
|
||||
server_name localhost;
|
||||
charset utf-8;
|
||||
|
||||
# Proxy auth for collaboration server
|
||||
location /collaboration/ws/ {
|
||||
# Collaboration Auth request configuration
|
||||
auth_request /collaboration-auth;
|
||||
auth_request_set $authHeader $upstream_http_authorization;
|
||||
auth_request_set $canEdit $upstream_http_x_can_edit;
|
||||
auth_request_set $userId $upstream_http_x_user_id;
|
||||
|
||||
# Pass specific headers from the auth response
|
||||
proxy_set_header Authorization $authHeader;
|
||||
proxy_set_header X-Can-Edit $canEdit;
|
||||
proxy_set_header X-User-Id $userId;
|
||||
|
||||
# Ensure WebSocket upgrade
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
|
||||
# Collaboration server
|
||||
proxy_pass http://y-provider:4444;
|
||||
|
||||
# Set appropriate timeout for WebSocket
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
|
||||
# Preserve original host and additional headers
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /collaboration-auth {
|
||||
proxy_pass http://app-dev:8000/api/v1.0/documents/collaboration-auth/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Original-URL $request_uri;
|
||||
|
||||
# Prevent the body from being passed
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-Original-Method $request_method;
|
||||
}
|
||||
|
||||
location /collaboration/api/ {
|
||||
# Collaboration server
|
||||
proxy_pass http://y-provider:4444;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Proxy auth for media
|
||||
location /media/ {
|
||||
# Auth request configuration
|
||||
|
||||
@@ -55,10 +55,11 @@ AI_API_KEY=password
|
||||
AI_MODEL=llama
|
||||
|
||||
# Collaboration
|
||||
COLLABORATION_API_URL=http://nginx:8083/collaboration/api/
|
||||
COLLABORATION_API_URL=http://y-provider:4444/collaboration/api/
|
||||
COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000
|
||||
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
|
||||
COLLABORATION_SERVER_SECRET=my-secret
|
||||
COLLABORATION_WS_URL=ws://localhost:8083/collaboration/ws/
|
||||
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
|
||||
|
||||
# Frontend
|
||||
FRONTEND_THEME=dsfr
|
||||
FRONTEND_THEME=default
|
||||
|
||||
@@ -151,6 +151,8 @@ class DocumentAdmin(TreeAdmin):
|
||||
"path",
|
||||
"depth",
|
||||
"numchild",
|
||||
"duplicated_from",
|
||||
"attachments",
|
||||
)
|
||||
},
|
||||
),
|
||||
@@ -166,8 +168,10 @@ class DocumentAdmin(TreeAdmin):
|
||||
"updated_at",
|
||||
)
|
||||
readonly_fields = (
|
||||
"attachments",
|
||||
"creator",
|
||||
"depth",
|
||||
"duplicated_from",
|
||||
"id",
|
||||
"numchild",
|
||||
"path",
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
import magic
|
||||
from rest_framework import exceptions, serializers
|
||||
|
||||
from core import enums, models
|
||||
from core import enums, models, utils
|
||||
from core.services.ai_services import AI_ACTIONS
|
||||
from core.services.converter_services import (
|
||||
ConversionError,
|
||||
@@ -27,6 +27,26 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ["id", "email", "full_name", "short_name"]
|
||||
|
||||
|
||||
class UserLightSerializer(UserSerializer):
|
||||
"""Serialize users with limited fields."""
|
||||
|
||||
id = serializers.SerializerMethodField(read_only=True)
|
||||
email = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_id(self, _user):
|
||||
"""Return always None. Here to have the same fields than in UserSerializer."""
|
||||
return None
|
||||
|
||||
def get_email(self, _user):
|
||||
"""Return always None. Here to have the same fields than in UserSerializer."""
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["id", "email", "full_name", "short_name"]
|
||||
read_only_fields = ["id", "email", "full_name", "short_name"]
|
||||
|
||||
|
||||
class BaseAccessSerializer(serializers.ModelSerializer):
|
||||
"""Serialize template accesses."""
|
||||
|
||||
@@ -118,6 +138,17 @@ class DocumentAccessSerializer(BaseAccessSerializer):
|
||||
read_only_fields = ["id", "abilities"]
|
||||
|
||||
|
||||
class DocumentAccessLightSerializer(DocumentAccessSerializer):
|
||||
"""Serialize document accesses with limited fields."""
|
||||
|
||||
user = UserLightSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
fields = ["id", "user", "team", "role", "abilities"]
|
||||
read_only_fields = ["id", "team", "role", "abilities"]
|
||||
|
||||
|
||||
class TemplateAccessSerializer(BaseAccessSerializer):
|
||||
"""Serialize template accesses."""
|
||||
|
||||
@@ -268,6 +299,53 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
|
||||
return value
|
||||
|
||||
def save(self, **kwargs):
|
||||
"""
|
||||
Process the content field to extract attachment keys and update the document's
|
||||
"attachments" field for access control.
|
||||
"""
|
||||
content = self.validated_data.get("content", "")
|
||||
extracted_attachments = set(utils.extract_attachments(content))
|
||||
|
||||
existing_attachments = (
|
||||
set(self.instance.attachments or []) if self.instance else set()
|
||||
)
|
||||
new_attachments = extracted_attachments - existing_attachments
|
||||
|
||||
if new_attachments:
|
||||
attachments_documents = (
|
||||
models.Document.objects.filter(
|
||||
attachments__overlap=list(new_attachments)
|
||||
)
|
||||
.only("path", "attachments")
|
||||
.order_by("path")
|
||||
)
|
||||
|
||||
user = self.context["request"].user
|
||||
readable_per_se_paths = (
|
||||
models.Document.objects.readable_per_se(user)
|
||||
.order_by("path")
|
||||
.values_list("path", flat=True)
|
||||
)
|
||||
readable_attachments_paths = utils.filter_descendants(
|
||||
[doc.path for doc in attachments_documents],
|
||||
readable_per_se_paths,
|
||||
skip_sorting=True,
|
||||
)
|
||||
|
||||
readable_attachments = set()
|
||||
for document in attachments_documents:
|
||||
if document.path not in readable_attachments_paths:
|
||||
continue
|
||||
readable_attachments.update(set(document.attachments) & new_attachments)
|
||||
|
||||
# Update attachments with readable keys
|
||||
self.validated_data["attachments"] = list(
|
||||
existing_attachments | readable_attachments
|
||||
)
|
||||
|
||||
return super().save(**kwargs)
|
||||
|
||||
|
||||
class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
"""
|
||||
@@ -381,6 +459,27 @@ class LinkDocumentSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class DocumentDuplicationSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for duplicating a document.
|
||||
Allows specifying whether to keep access permissions.
|
||||
"""
|
||||
|
||||
with_accesses = serializers.BooleanField(default=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
This serializer is not intended to create objects.
|
||||
"""
|
||||
raise NotImplementedError("This serializer does not support creation.")
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
This serializer is not intended to update objects.
|
||||
"""
|
||||
raise NotImplementedError("This serializer does not support updating.")
|
||||
|
||||
|
||||
# Suppress the warning about not implementing `create` and `update` methods
|
||||
# since we don't use a model and only rely on the serializer for validation
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
@@ -17,6 +16,8 @@ from django.db import transaction
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Left, Length
|
||||
from django.http import Http404, StreamingHttpResponse
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import requests
|
||||
import rest_framework as drf
|
||||
@@ -29,23 +30,13 @@ from rest_framework.throttling import UserRateThrottle
|
||||
from core import authentication, enums, models
|
||||
from core.services.ai_services import AIService
|
||||
from core.services.collaboration_services import CollaborationService
|
||||
from core.utils import extract_attachments, filter_descendants
|
||||
|
||||
from . import permissions, serializers, utils
|
||||
from .filters import DocumentFilter, ListDocumentFilter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ATTACHMENTS_FOLDER = "attachments"
|
||||
UUID_REGEX = (
|
||||
r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
|
||||
)
|
||||
FILE_EXT_REGEX = r"\.[a-zA-Z0-9]{1,10}"
|
||||
MEDIA_STORAGE_URL_PATTERN = re.compile(
|
||||
f"{settings.MEDIA_URL:s}(?P<pk>{UUID_REGEX:s})/"
|
||||
f"(?P<key>{ATTACHMENTS_FOLDER:s}/{UUID_REGEX:s}(?:-unsafe)?{FILE_EXT_REGEX:s})$"
|
||||
)
|
||||
COLLABORATION_WS_URL_PATTERN = re.compile(rf"(?:^|&)room=(?P<pk>{UUID_REGEX})(?:&|$)")
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
|
||||
|
||||
@@ -389,10 +380,7 @@ class DocumentViewSet(
|
||||
9. **Media Auth**: Authorize access to document media.
|
||||
Example: GET /documents/media-auth/
|
||||
|
||||
10. **Collaboration Auth**: Authorize access to the collaboration server for a document.
|
||||
Example: GET /documents/collaboration-auth/
|
||||
|
||||
11. **AI Transform**: Apply a transformation action on a piece of text with AI.
|
||||
10. **AI Transform**: Apply a transformation action on a piece of text with AI.
|
||||
Example: POST /documents/{id}/ai-transform/
|
||||
Expected data:
|
||||
- text (str): The input text.
|
||||
@@ -400,7 +388,7 @@ class DocumentViewSet(
|
||||
Returns: JSON response with the processed text.
|
||||
Throttled by: AIDocumentRateThrottle, AIUserRateThrottle.
|
||||
|
||||
12. **AI Translate**: Translate a piece of text with AI.
|
||||
11. **AI Translate**: Translate a piece of text with AI.
|
||||
Example: POST /documents/{id}/ai-translate/
|
||||
Expected data:
|
||||
- text (str): The input text.
|
||||
@@ -855,14 +843,15 @@ class DocumentViewSet(
|
||||
)
|
||||
|
||||
# Get the highest readable ancestor
|
||||
highest_readable = ancestors.readable_per_se(request.user).only("depth").first()
|
||||
highest_readable = (
|
||||
ancestors.readable_per_se(request.user).only("depth", "path").first()
|
||||
)
|
||||
if highest_readable is None:
|
||||
raise (
|
||||
drf.exceptions.PermissionDenied()
|
||||
if request.user.is_authenticated
|
||||
else drf.exceptions.NotAuthenticated()
|
||||
)
|
||||
|
||||
paths_links_mapping = {}
|
||||
ancestors_links = []
|
||||
children_clause = db.Q()
|
||||
@@ -885,6 +874,17 @@ class DocumentViewSet(
|
||||
|
||||
queryset = ancestors.filter(depth__gte=highest_readable.depth) | children
|
||||
queryset = queryset.order_by("path")
|
||||
# Annotate if the current document is the highest ancestor for the user
|
||||
queryset = queryset.annotate(
|
||||
is_highest_ancestor_for_user=db.Case(
|
||||
db.When(
|
||||
path=db.Value(highest_readable.path),
|
||||
then=db.Value(True),
|
||||
),
|
||||
default=db.Value(False),
|
||||
output_field=db.BooleanField(),
|
||||
)
|
||||
)
|
||||
queryset = self.annotate_user_roles(queryset)
|
||||
queryset = self.annotate_is_favorite(queryset)
|
||||
|
||||
@@ -902,6 +902,82 @@ class DocumentViewSet(
|
||||
utils.nest_tree(serializer.data, self.queryset.model.steplen)
|
||||
)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
permission_classes=[permissions.IsAuthenticated, permissions.AccessPermission],
|
||||
url_path="duplicate",
|
||||
)
|
||||
@transaction.atomic
|
||||
def duplicate(self, request, *args, **kwargs):
|
||||
"""
|
||||
Duplicate a document and store the links to attached files in the duplicated
|
||||
document to allow cross-access.
|
||||
|
||||
Optionally duplicates accesses if `with_accesses` is set to true
|
||||
in the payload.
|
||||
"""
|
||||
# Get document while checking permissions
|
||||
document = self.get_object()
|
||||
|
||||
serializer = serializers.DocumentDuplicationSerializer(
|
||||
data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
with_accesses = serializer.validated_data.get("with_accesses", False)
|
||||
|
||||
base64_yjs_content = document.content
|
||||
|
||||
# Duplicate the document instance
|
||||
link_kwargs = (
|
||||
{"link_reach": document.link_reach, "link_role": document.link_role}
|
||||
if with_accesses
|
||||
else {}
|
||||
)
|
||||
extracted_attachments = set(extract_attachments(document.content))
|
||||
attachments = list(extracted_attachments & set(document.attachments))
|
||||
duplicated_document = document.add_sibling(
|
||||
"right",
|
||||
title=capfirst(_("copy of {title}").format(title=document.title)),
|
||||
content=base64_yjs_content,
|
||||
attachments=attachments,
|
||||
duplicated_from=document,
|
||||
creator=request.user,
|
||||
**link_kwargs,
|
||||
)
|
||||
|
||||
# Always add the logged-in user as OWNER
|
||||
accesses_to_create = [
|
||||
models.DocumentAccess(
|
||||
document=duplicated_document,
|
||||
user=request.user,
|
||||
role=models.RoleChoices.OWNER,
|
||||
)
|
||||
]
|
||||
|
||||
# If accesses should be duplicated, add other users' accesses as per original document
|
||||
if with_accesses:
|
||||
original_accesses = models.DocumentAccess.objects.filter(
|
||||
document=document
|
||||
).exclude(user=request.user)
|
||||
|
||||
accesses_to_create.extend(
|
||||
models.DocumentAccess(
|
||||
document=duplicated_document,
|
||||
user_id=access.user_id,
|
||||
team=access.team,
|
||||
role=access.role,
|
||||
)
|
||||
for access in original_accesses
|
||||
)
|
||||
|
||||
# Bulk create all the duplicated accesses
|
||||
models.DocumentAccess.objects.bulk_create(accesses_to_create)
|
||||
|
||||
return drf_response.Response(
|
||||
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
@drf.decorators.action(detail=True, methods=["get"], url_path="versions")
|
||||
def versions_list(self, request, *args, **kwargs):
|
||||
"""
|
||||
@@ -943,7 +1019,7 @@ class DocumentViewSet(
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["get", "delete"],
|
||||
url_path="versions/(?P<version_id>[0-9a-f-]{36})",
|
||||
url_path="versions/(?P<version_id>[0-9a-z-]+)",
|
||||
)
|
||||
# pylint: disable=unused-argument
|
||||
def versions_detail(self, request, pk, version_id, *args, **kwargs):
|
||||
@@ -1051,7 +1127,7 @@ class DocumentViewSet(
|
||||
|
||||
# Generate a generic yet unique filename to store the image in object storage
|
||||
file_id = uuid.uuid4()
|
||||
extension = serializer.validated_data["expected_extension"]
|
||||
ext = serializer.validated_data["expected_extension"]
|
||||
|
||||
# Prepare metadata for storage
|
||||
extra_args = {
|
||||
@@ -1063,7 +1139,7 @@ class DocumentViewSet(
|
||||
extra_args["Metadata"]["is_unsafe"] = "true"
|
||||
file_unsafe = "-unsafe"
|
||||
|
||||
key = f"{document.key_base}/{ATTACHMENTS_FOLDER:s}/{file_id!s}{file_unsafe}.{extension:s}"
|
||||
key = f"{document.key_base}/{enums.ATTACHMENTS_FOLDER:s}/{file_id!s}{file_unsafe}.{ext:s}"
|
||||
|
||||
file_name = serializer.validated_data["file_name"]
|
||||
if (
|
||||
@@ -1083,15 +1159,19 @@ class DocumentViewSet(
|
||||
file, default_storage.bucket_name, key, ExtraArgs=extra_args
|
||||
)
|
||||
|
||||
# Make the attachment readable by document readers
|
||||
document.attachments.append(key)
|
||||
document.save()
|
||||
|
||||
return drf.response.Response(
|
||||
{"file": f"{settings.MEDIA_URL:s}{key:s}"},
|
||||
status=drf.status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
def _authorize_subrequest(self, request, pattern):
|
||||
def _auth_get_original_url(self, request):
|
||||
"""
|
||||
Shared method to authorize access based on the original URL of an Nginx subrequest
|
||||
and user permissions. Returns a dictionary of URL parameters if authorized.
|
||||
Extracts and parses the original URL from the "HTTP_X_ORIGINAL_URL" header.
|
||||
Raises PermissionDenied if the header is missing.
|
||||
|
||||
The original url is passed by nginx in the "HTTP_X_ORIGINAL_URL" header.
|
||||
See corresponding ingress configuration in Helm chart and read about the
|
||||
@@ -1102,14 +1182,6 @@ class DocumentViewSet(
|
||||
to let this request go through (by returning a 200 code) or if we block it (by returning
|
||||
a 403 error). Note that we return 403 errors without any further details for security
|
||||
reasons.
|
||||
|
||||
Parameters:
|
||||
- pattern: The regex pattern to extract identifiers from the URL.
|
||||
|
||||
Returns:
|
||||
- A dictionary of URL parameters if the request is authorized.
|
||||
Raises:
|
||||
- PermissionDenied if authorization fails.
|
||||
"""
|
||||
# Extract the original URL from the request header
|
||||
original_url = request.META.get("HTTP_X_ORIGINAL_URL")
|
||||
@@ -1117,52 +1189,21 @@ class DocumentViewSet(
|
||||
logger.debug("Missing HTTP_X_ORIGINAL_URL header in subrequest")
|
||||
raise drf.exceptions.PermissionDenied()
|
||||
|
||||
parsed_url = urlparse(original_url)
|
||||
match = pattern.search(parsed_url.path)
|
||||
|
||||
# If the path does not match the pattern, try to extract the parameters from the query
|
||||
if not match:
|
||||
match = pattern.search(parsed_url.query)
|
||||
|
||||
if not match:
|
||||
logger.debug(
|
||||
"Subrequest URL '%s' did not match pattern '%s'",
|
||||
parsed_url.path,
|
||||
pattern,
|
||||
)
|
||||
raise drf.exceptions.PermissionDenied()
|
||||
logger.debug("Original url: '%s'", original_url)
|
||||
return urlparse(original_url)
|
||||
|
||||
def _auth_get_url_params(self, pattern, fragment):
|
||||
"""
|
||||
Extracts URL parameters from the given fragment using the specified regex pattern.
|
||||
Raises PermissionDenied if parameters cannot be extracted.
|
||||
"""
|
||||
match = pattern.search(fragment)
|
||||
try:
|
||||
url_params = match.groupdict()
|
||||
return match.groupdict()
|
||||
except (ValueError, AttributeError) as exc:
|
||||
logger.debug("Failed to extract parameters from subrequest URL: %s", exc)
|
||||
raise drf.exceptions.PermissionDenied() from exc
|
||||
|
||||
pk = url_params.get("pk")
|
||||
if not pk:
|
||||
logger.debug("Document ID (pk) not found in URL parameters: %s", url_params)
|
||||
raise drf.exceptions.PermissionDenied()
|
||||
|
||||
# Fetch the document and check if the user has access
|
||||
try:
|
||||
document = models.Document.objects.get(pk=pk)
|
||||
except models.Document.DoesNotExist as exc:
|
||||
logger.debug("Document with ID '%s' does not exist", pk)
|
||||
raise drf.exceptions.PermissionDenied() from exc
|
||||
|
||||
user_abilities = document.get_abilities(request.user)
|
||||
|
||||
if not user_abilities.get(self.action, False):
|
||||
logger.debug(
|
||||
"User '%s' lacks permission for document '%s'", request.user, pk
|
||||
)
|
||||
raise drf.exceptions.PermissionDenied()
|
||||
|
||||
logger.debug(
|
||||
"Subrequest authorization successful. Extracted parameters: %s", url_params
|
||||
)
|
||||
return url_params, user_abilities, request.user.id
|
||||
|
||||
@drf.decorators.action(detail=False, methods=["get"], url_path="media-auth")
|
||||
def media_auth(self, request, *args, **kwargs):
|
||||
"""
|
||||
@@ -1174,36 +1215,42 @@ class DocumentViewSet(
|
||||
annotation. The request will then be proxied to the object storage backend who will
|
||||
respond with the file after checking the signature included in headers.
|
||||
"""
|
||||
url_params, _, _ = self._authorize_subrequest(
|
||||
request, MEDIA_STORAGE_URL_PATTERN
|
||||
parsed_url = self._auth_get_original_url(request)
|
||||
url_params = self._auth_get_url_params(
|
||||
enums.MEDIA_STORAGE_URL_PATTERN, parsed_url.path
|
||||
)
|
||||
pk, key = url_params.values()
|
||||
|
||||
user = request.user
|
||||
key = f"{url_params['pk']:s}/{url_params['attachment']:s}"
|
||||
|
||||
# Look for a document to which the user has access and that includes this attachment
|
||||
# We must look into all descendants of any document to which the user has access per se
|
||||
readable_per_se_paths = (
|
||||
self.queryset.readable_per_se(user)
|
||||
.order_by("path")
|
||||
.values_list("path", flat=True)
|
||||
)
|
||||
|
||||
attachments_documents = (
|
||||
self.queryset.filter(attachments__contains=[key])
|
||||
.only("path")
|
||||
.order_by("path")
|
||||
)
|
||||
readable_attachments_paths = filter_descendants(
|
||||
[doc.path for doc in attachments_documents],
|
||||
readable_per_se_paths,
|
||||
skip_sorting=True,
|
||||
)
|
||||
|
||||
if not readable_attachments_paths:
|
||||
logger.debug("User '%s' lacks permission for attachment", user)
|
||||
raise drf.exceptions.PermissionDenied()
|
||||
|
||||
# Generate S3 authorization headers using the extracted URL parameters
|
||||
request = utils.generate_s3_authorization_headers(f"{pk:s}/{key:s}")
|
||||
request = utils.generate_s3_authorization_headers(key)
|
||||
|
||||
return drf.response.Response("authorized", headers=request.headers, status=200)
|
||||
|
||||
@drf.decorators.action(detail=False, methods=["get"], url_path="collaboration-auth")
|
||||
def collaboration_auth(self, request, *args, **kwargs):
|
||||
"""
|
||||
This view is used by an Nginx subrequest to control access to a document's
|
||||
collaboration server.
|
||||
"""
|
||||
_, user_abilities, user_id = self._authorize_subrequest(
|
||||
request, COLLABORATION_WS_URL_PATTERN
|
||||
)
|
||||
can_edit = user_abilities["partial_update"]
|
||||
|
||||
# Add the collaboration server secret token to the headers
|
||||
headers = {
|
||||
"Authorization": settings.COLLABORATION_SERVER_SECRET,
|
||||
"X-Can-Edit": str(can_edit),
|
||||
"X-User-Id": str(user_id),
|
||||
}
|
||||
|
||||
return drf.response.Response("authorized", headers=headers, status=200)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
@@ -1323,12 +1370,7 @@ class DocumentViewSet(
|
||||
|
||||
class DocumentAccessViewSet(
|
||||
ResourceAccessViewsetMixin,
|
||||
drf.mixins.CreateModelMixin,
|
||||
drf.mixins.DestroyModelMixin,
|
||||
drf.mixins.ListModelMixin,
|
||||
drf.mixins.RetrieveModelMixin,
|
||||
drf.mixins.UpdateModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
viewsets.ModelViewSet,
|
||||
):
|
||||
"""
|
||||
API ViewSet for all interactions with document accesses.
|
||||
@@ -1360,6 +1402,32 @@ class DocumentAccessViewSet(
|
||||
queryset = models.DocumentAccess.objects.select_related("user").all()
|
||||
resource_field_name = "document"
|
||||
serializer_class = serializers.DocumentAccessSerializer
|
||||
is_current_user_owner_or_admin = False
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset according to the action."""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
if self.action == "list":
|
||||
try:
|
||||
document = models.Document.objects.get(pk=self.kwargs["resource_id"])
|
||||
except models.Document.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
roles = set(document.get_roles(self.request.user))
|
||||
is_owner_or_admin = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))
|
||||
self.is_current_user_owner_or_admin = is_owner_or_admin
|
||||
if not is_owner_or_admin:
|
||||
# Return only the document owner access
|
||||
queryset = queryset.filter(role__in=models.PRIVILEGED_ROLES)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "list" and not self.is_current_user_owner_or_admin:
|
||||
return serializers.DocumentAccessLightSerializer
|
||||
|
||||
return super().get_serializer_class()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Add a new access to the document and send an email to the new added user."""
|
||||
|
||||
@@ -2,10 +2,26 @@
|
||||
Core application enums declaration
|
||||
"""
|
||||
|
||||
from django.conf import global_settings
|
||||
import re
|
||||
|
||||
from django.conf import global_settings, settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
ATTACHMENTS_FOLDER = "attachments"
|
||||
UUID_REGEX = (
|
||||
r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
|
||||
)
|
||||
FILE_EXT_REGEX = r"\.[a-zA-Z0-9]{1,10}"
|
||||
MEDIA_STORAGE_URL_PATTERN = re.compile(
|
||||
f"{settings.MEDIA_URL:s}(?P<pk>{UUID_REGEX:s})/"
|
||||
f"(?P<attachment>{ATTACHMENTS_FOLDER:s}/{UUID_REGEX:s}(?:-unsafe)?{FILE_EXT_REGEX:s})$"
|
||||
)
|
||||
MEDIA_STORAGE_URL_EXTRACT = re.compile(
|
||||
f"{settings.MEDIA_URL:s}({UUID_REGEX}/{ATTACHMENTS_FOLDER}/{UUID_REGEX}{FILE_EXT_REGEX})"
|
||||
)
|
||||
|
||||
|
||||
# In Django's code base, `LANGUAGES` is set by default with all supported languages.
|
||||
# We can use it for the choice of languages which should not be limited to the few languages
|
||||
# active in the app.
|
||||
|
||||
@@ -13,6 +13,22 @@ from core import models
|
||||
|
||||
fake = Faker()
|
||||
|
||||
YDOC_HELLO_WORLD_BASE64 = (
|
||||
"AR717vLVDgAHAQ5kb2N1bWVudC1zdG9yZQMKYmxvY2tHcm91cAcA9e7y1Q4AAw5ibG9ja0NvbnRh"
|
||||
"aW5lcgcA9e7y1Q4BAwdoZWFkaW5nBwD17vLVDgIGBgD17vLVDgMGaXRhbGljAnt9hPXu8tUOBAVI"
|
||||
"ZWxsb4b17vLVDgkGaXRhbGljBG51bGwoAPXu8tUOAg10ZXh0QWxpZ25tZW50AXcEbGVmdCgA9e7y"
|
||||
"1Q4CBWxldmVsAX0BKAD17vLVDgECaWQBdyQwNGQ2MjM0MS04MzI2LTQyMzYtYTA4My00ODdlMjZm"
|
||||
"YWQyMzAoAPXu8tUOAQl0ZXh0Q29sb3IBdwdkZWZhdWx0KAD17vLVDgEPYmFja2dyb3VuZENvbG9y"
|
||||
"AXcHZGVmYXVsdIf17vLVDgEDDmJsb2NrQ29udGFpbmVyBwD17vLVDhADDmJ1bGxldExpc3RJdGVt"
|
||||
"BwD17vLVDhEGBAD17vLVDhIBd4b17vLVDhMEYm9sZAJ7fYT17vLVDhQCb3KG9e7y1Q4WBGJvbGQE"
|
||||
"bnVsbIT17vLVDhcCbGQoAPXu8tUOEQ10ZXh0QWxpZ25tZW50AXcEbGVmdCgA9e7y1Q4QAmlkAXck"
|
||||
"ZDM1MWUwNjgtM2U1NS00MjI2LThlYTUtYWJiMjYzMTk4ZTJhKAD17vLVDhAJdGV4dENvbG9yAXcH"
|
||||
"ZGVmYXVsdCgA9e7y1Q4QD2JhY2tncm91bmRDb2xvcgF3B2RlZmF1bHSH9e7y1Q4QAw5ibG9ja0Nv"
|
||||
"bnRhaW5lcgcA9e7y1Q4eAwlwYXJhZ3JhcGgoAPXu8tUOHw10ZXh0QWxpZ25tZW50AXcEbGVmdCgA"
|
||||
"9e7y1Q4eAmlkAXckODk3MDBjMDctZTBlMS00ZmUwLWFjYTItODQ5MzIwOWE3ZTQyKAD17vLVDh4J"
|
||||
"dGV4dENvbG9yAXcHZGVmYXVsdCgA9e7y1Q4eD2JhY2tncm91bmRDb2xvcgF3B2RlZmF1bHQA"
|
||||
)
|
||||
|
||||
|
||||
class UserFactory(factory.django.DjangoModelFactory):
|
||||
"""A factory to random users for testing purposes."""
|
||||
@@ -75,7 +91,7 @@ class DocumentFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
title = factory.Sequence(lambda n: f"document{n}")
|
||||
excerpt = factory.Sequence(lambda n: f"excerpt{n}")
|
||||
content = factory.Sequence(lambda n: f"content{n}")
|
||||
content = YDOC_HELLO_WORLD_BASE64
|
||||
creator = factory.SubFactory(UserFactory)
|
||||
deleted_at = None
|
||||
link_reach = factory.fuzzy.FuzzyChoice(
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-18 11:53
|
||||
import re
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
import django.db.models.deletion
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import migrations, models
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
import core.models
|
||||
from core.utils import extract_attachments
|
||||
|
||||
|
||||
def populate_attachments_on_all_documents(apps, schema_editor):
|
||||
"""Populate "attachments" field on all existing documents in the database."""
|
||||
Document = apps.get_model("core", "Document")
|
||||
|
||||
for document in Document.objects.all():
|
||||
try:
|
||||
response = default_storage.connection.meta.client.get_object(
|
||||
Bucket=default_storage.bucket_name, Key=f"{document.pk!s}/file"
|
||||
)
|
||||
except (FileNotFoundError, ClientError):
|
||||
pass
|
||||
else:
|
||||
content = response["Body"].read().decode("utf-8")
|
||||
document.attachments = extract_attachments(content)
|
||||
document.save(update_fields=["attachments"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0019_alter_user_language_default_to_null"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# v2.0.0 was released so we can now remove BC field "is_public"
|
||||
migrations.RemoveField(
|
||||
model_name="document",
|
||||
name="is_public",
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name="user",
|
||||
managers=[
|
||||
("objects", core.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="document",
|
||||
name="attachments",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(max_length=255),
|
||||
blank=True,
|
||||
default=list,
|
||||
editable=False,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="document",
|
||||
name="duplicated_from",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="duplicates",
|
||||
to="core.document",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
populate_attachments_on_all_documents,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
@@ -13,6 +13,7 @@ from logging import getLogger
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import models as auth_models
|
||||
from django.contrib.auth.base_user import AbstractBaseUser
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core import mail, validators
|
||||
from django.core.cache import cache
|
||||
@@ -23,7 +24,7 @@ from django.db import models, transaction
|
||||
from django.db.models.functions import Left, Length
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property, lazy
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import get_language, override
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -96,7 +97,7 @@ class LinkReachChoices(models.TextChoices):
|
||||
"""
|
||||
# If no ancestors, return all options
|
||||
if not ancestors_links:
|
||||
return {reach: LinkRoleChoices.values for reach in cls.values}
|
||||
return dict.fromkeys(cls.values, LinkRoleChoices.values)
|
||||
|
||||
# Initialize result with all possible reaches and role options as sets
|
||||
result = {reach: set(LinkRoleChoices.values) for reach in cls.values}
|
||||
@@ -243,7 +244,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
|
||||
language = models.CharField(
|
||||
max_length=10,
|
||||
choices=lazy(lambda: settings.LANGUAGES, tuple)(),
|
||||
choices=settings.LANGUAGES,
|
||||
default=None,
|
||||
verbose_name=_("language"),
|
||||
help_text=_("The language in which the user wants to see the interface."),
|
||||
@@ -363,10 +364,9 @@ class BaseAccess(BaseModel):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def _get_abilities(self, resource, user):
|
||||
def _get_roles(self, resource, user):
|
||||
"""
|
||||
Compute and return abilities for a given user taking into account
|
||||
the current state of the object.
|
||||
Get the roles a user has on a resource.
|
||||
"""
|
||||
roles = []
|
||||
if user.is_authenticated:
|
||||
@@ -381,6 +381,15 @@ class BaseAccess(BaseModel):
|
||||
except (self._meta.model.DoesNotExist, IndexError):
|
||||
roles = []
|
||||
|
||||
return roles
|
||||
|
||||
def _get_abilities(self, resource, user):
|
||||
"""
|
||||
Compute and return abilities for a given user taking into account
|
||||
the current state of the object.
|
||||
"""
|
||||
roles = self._get_roles(resource, user)
|
||||
|
||||
is_owner_or_admin = bool(
|
||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||
)
|
||||
@@ -427,10 +436,12 @@ class DocumentQuerySet(MP_NodeQuerySet):
|
||||
|
||||
def readable_per_se(self, user):
|
||||
"""
|
||||
Filters the queryset to return documents that the given user has
|
||||
permission to read.
|
||||
Filters the queryset to return documents on which the given user has
|
||||
direct access, team access or link access. This will not return all the
|
||||
documents that a user can read because it can be obtained via an ancestor.
|
||||
:param user: The user for whom readable documents are to be fetched.
|
||||
:return: A queryset of documents readable by the user.
|
||||
:return: A queryset of documents for which the user has direct access,
|
||||
team access or link access.
|
||||
"""
|
||||
if user.is_authenticated:
|
||||
return self.filter(
|
||||
@@ -442,26 +453,15 @@ class DocumentQuerySet(MP_NodeQuerySet):
|
||||
return self.filter(link_reach=LinkReachChoices.PUBLIC)
|
||||
|
||||
|
||||
class DocumentManager(MP_NodeManager):
|
||||
class DocumentManager(MP_NodeManager.from_queryset(DocumentQuerySet)):
|
||||
"""
|
||||
Custom manager for the Document model, enabling the use of the custom
|
||||
queryset methods directly from the model manager.
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Overrides the default get_queryset method to return a custom queryset.
|
||||
:return: An instance of DocumentQuerySet.
|
||||
"""
|
||||
return DocumentQuerySet(self.model, using=self._db)
|
||||
|
||||
def readable_per_se(self, user):
|
||||
"""
|
||||
Filters documents based on user permissions using the custom queryset.
|
||||
:param user: The user for whom readable documents are to be fetched.
|
||||
:return: A queryset of documents readable by the user.
|
||||
"""
|
||||
return self.get_queryset().readable_per_se(user)
|
||||
"""Sets the custom queryset as the default."""
|
||||
return self._queryset_class(self.model).order_by("path")
|
||||
|
||||
|
||||
class Document(MP_Node, BaseModel):
|
||||
@@ -486,6 +486,21 @@ class Document(MP_Node, BaseModel):
|
||||
)
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
ancestors_deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
duplicated_from = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="duplicates",
|
||||
editable=False,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
attachments = ArrayField(
|
||||
models.CharField(max_length=255),
|
||||
default=list,
|
||||
editable=False,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
_content = None
|
||||
|
||||
@@ -582,9 +597,13 @@ class Document(MP_Node, BaseModel):
|
||||
|
||||
def get_content_response(self, version_id=""):
|
||||
"""Get the content in a specific version of the document"""
|
||||
return default_storage.connection.meta.client.get_object(
|
||||
Bucket=default_storage.bucket_name, Key=self.file_key, VersionId=version_id
|
||||
)
|
||||
params = {
|
||||
"Bucket": default_storage.bucket_name,
|
||||
"Key": self.file_key,
|
||||
}
|
||||
if version_id:
|
||||
params["VersionId"] = version_id
|
||||
return default_storage.connection.meta.client.get_object(**params)
|
||||
|
||||
def get_versions_slice(self, from_version_id="", min_datetime=None, page_size=None):
|
||||
"""Get document versions from object storage with pagination and starting conditions"""
|
||||
@@ -730,6 +749,32 @@ class Document(MP_Node, BaseModel):
|
||||
|
||||
return dict(links_definitions) # Convert defaultdict back to a normal dict
|
||||
|
||||
def compute_ancestors_links(self, user):
|
||||
"""
|
||||
Compute the ancestors links for the current document up to the highest readable ancestor.
|
||||
"""
|
||||
ancestors = (
|
||||
(self.get_ancestors() | self._meta.model.objects.filter(pk=self.pk))
|
||||
.filter(ancestors_deleted_at__isnull=True)
|
||||
.order_by("path")
|
||||
)
|
||||
highest_readable = ancestors.readable_per_se(user).only("depth").first()
|
||||
|
||||
if highest_readable is None:
|
||||
return []
|
||||
|
||||
ancestors_links = []
|
||||
paths_links_mapping = {}
|
||||
for ancestor in ancestors.filter(depth__gte=highest_readable.depth):
|
||||
ancestors_links.append(
|
||||
{"link_reach": ancestor.link_reach, "link_role": ancestor.link_role}
|
||||
)
|
||||
paths_links_mapping[ancestor.path] = ancestors_links.copy()
|
||||
|
||||
ancestors_links = paths_links_mapping.get(self.path[: -self.steplen], [])
|
||||
|
||||
return ancestors_links
|
||||
|
||||
def get_abilities(self, user, ancestors_links=None):
|
||||
"""
|
||||
Compute and return abilities for a given user on the document.
|
||||
@@ -737,7 +782,7 @@ class Document(MP_Node, BaseModel):
|
||||
if self.depth <= 1 or getattr(self, "is_highest_ancestor_for_user", False):
|
||||
ancestors_links = []
|
||||
elif ancestors_links is None:
|
||||
ancestors_links = self.get_ancestors().values("link_reach", "link_role")
|
||||
ancestors_links = self.compute_ancestors_links(user=user)
|
||||
|
||||
roles = set(
|
||||
self.get_roles(user)
|
||||
@@ -796,6 +841,7 @@ class Document(MP_Node, BaseModel):
|
||||
"cors_proxy": can_get,
|
||||
"descendants": can_get,
|
||||
"destroy": is_owner,
|
||||
"duplicate": can_get,
|
||||
"favorite": can_get and user.is_authenticated,
|
||||
"link_configuration": is_owner_or_admin,
|
||||
"invite_owner": is_owner,
|
||||
@@ -1065,7 +1111,41 @@ class DocumentAccess(BaseAccess):
|
||||
"""
|
||||
Compute and return abilities for a given user on the document access.
|
||||
"""
|
||||
return self._get_abilities(self.document, user)
|
||||
roles = self._get_roles(self.document, user)
|
||||
is_owner_or_admin = bool(set(roles).intersection(set(PRIVILEGED_ROLES)))
|
||||
if self.role == RoleChoices.OWNER:
|
||||
can_delete = (
|
||||
RoleChoices.OWNER in roles
|
||||
and self.document.accesses.filter(role=RoleChoices.OWNER).count() > 1
|
||||
)
|
||||
set_role_to = (
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
if can_delete
|
||||
else []
|
||||
)
|
||||
else:
|
||||
can_delete = is_owner_or_admin
|
||||
set_role_to = []
|
||||
if RoleChoices.OWNER in roles:
|
||||
set_role_to.append(RoleChoices.OWNER)
|
||||
if is_owner_or_admin:
|
||||
set_role_to.extend(
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
)
|
||||
|
||||
# Remove the current role as we don't want to propose it as an option
|
||||
try:
|
||||
set_role_to.remove(self.role)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"destroy": can_delete,
|
||||
"update": bool(set_role_to) and is_owner_or_admin,
|
||||
"partial_update": bool(set_role_to) and is_owner_or_admin,
|
||||
"retrieve": self.user and self.user.id == user.id or is_owner_or_admin,
|
||||
"set_role_to": set_role_to,
|
||||
}
|
||||
|
||||
|
||||
class Template(BaseModel):
|
||||
|
||||
@@ -59,8 +59,32 @@ def test_api_document_accesses_list_authenticated_unrelated():
|
||||
}
|
||||
|
||||
|
||||
def test_api_document_accesses_list_unexisting_document():
|
||||
"""
|
||||
Listing document accesses for an unexisting document should return an empty list.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{uuid4()!s}/accesses/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
|
||||
@pytest.mark.parametrize(
|
||||
"role", [role for role in models.RoleChoices if role not in models.PRIVILEGED_ROLES]
|
||||
)
|
||||
def test_api_document_accesses_list_authenticated_related_non_privileged(
|
||||
via, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Authenticated users should be able to list document accesses for a document
|
||||
to which they are directly related, whatever their role in the document.
|
||||
@@ -70,24 +94,114 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
owner = factories.UserFactory()
|
||||
accesses = []
|
||||
|
||||
document_access = factories.UserDocumentAccessFactory(
|
||||
user=owner, role=models.RoleChoices.OWNER
|
||||
)
|
||||
accesses.append(document_access)
|
||||
document = document_access.document
|
||||
if via == USER:
|
||||
models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
user=user,
|
||||
role=role,
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
team="lasuite",
|
||||
role=role,
|
||||
)
|
||||
|
||||
access1 = factories.TeamDocumentAccessFactory(document=document)
|
||||
access2 = factories.UserDocumentAccessFactory(document=document)
|
||||
accesses.append(access1)
|
||||
accesses.append(access2)
|
||||
|
||||
# Accesses for other documents to which the user is related should not be listed either
|
||||
other_access = factories.UserDocumentAccessFactory(user=user)
|
||||
factories.UserDocumentAccessFactory(document=other_access.document)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
)
|
||||
|
||||
# Return only owners
|
||||
owners_accesses = [
|
||||
access for access in accesses if access.role in models.PRIVILEGED_ROLES
|
||||
]
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert content["count"] == len(owners_accesses)
|
||||
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access.id),
|
||||
"user": {
|
||||
"id": None,
|
||||
"email": None,
|
||||
"full_name": access.user.full_name,
|
||||
"short_name": access.user.short_name,
|
||||
}
|
||||
if access.user
|
||||
else None,
|
||||
"team": access.team,
|
||||
"role": access.role,
|
||||
"abilities": access.get_abilities(user),
|
||||
}
|
||||
for access in owners_accesses
|
||||
],
|
||||
key=lambda x: x["id"],
|
||||
)
|
||||
|
||||
for access in content["results"]:
|
||||
assert access["role"] in models.PRIVILEGED_ROLES
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize("role", models.PRIVILEGED_ROLES)
|
||||
def test_api_document_accesses_list_authenticated_related_privileged_roles(
|
||||
via, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Authenticated users should be able to list document accesses for a document
|
||||
to which they are directly related, whatever their role in the document.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
owner = factories.UserFactory()
|
||||
accesses = []
|
||||
|
||||
document_access = factories.UserDocumentAccessFactory(
|
||||
user=owner, role=models.RoleChoices.OWNER
|
||||
)
|
||||
accesses.append(document_access)
|
||||
document = document_access.document
|
||||
user_access = None
|
||||
if via == USER:
|
||||
user_access = models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
user=user,
|
||||
role=random.choice(models.RoleChoices.values),
|
||||
role=role,
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
user_access = models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
team="lasuite",
|
||||
role=random.choice(models.RoleChoices.values),
|
||||
role=role,
|
||||
)
|
||||
|
||||
access1 = factories.TeamDocumentAccessFactory(document=document)
|
||||
access2 = factories.UserDocumentAccessFactory(document=document)
|
||||
accesses.append(access1)
|
||||
accesses.append(access2)
|
||||
|
||||
# Accesses for other documents to which the user is related should not be listed either
|
||||
other_access = factories.UserDocumentAccessFactory(user=user)
|
||||
@@ -102,7 +216,7 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 3
|
||||
assert len(content["results"]) == 4
|
||||
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
@@ -126,6 +240,13 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
|
||||
"role": access2.role,
|
||||
"abilities": access2.get_abilities(user),
|
||||
},
|
||||
{
|
||||
"id": str(document_access.id),
|
||||
"user": serializers.UserSerializer(instance=owner).data,
|
||||
"team": "",
|
||||
"role": models.RoleChoices.OWNER,
|
||||
"abilities": document_access.get_abilities(user),
|
||||
},
|
||||
],
|
||||
key=lambda x: x["id"],
|
||||
)
|
||||
@@ -184,7 +305,10 @@ def test_api_document_accesses_retrieve_authenticated_unrelated():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_teams):
|
||||
@pytest.mark.parametrize("role", models.RoleChoices)
|
||||
def test_api_document_accesses_retrieve_authenticated_related(
|
||||
via, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
A user who is related to a document should be allowed to retrieve the
|
||||
associated document user accesses.
|
||||
@@ -196,10 +320,12 @@ def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_tea
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user)
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
access = factories.UserDocumentAccessFactory(document=document)
|
||||
|
||||
@@ -207,16 +333,19 @@ def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_tea
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
)
|
||||
|
||||
access_user = serializers.UserSerializer(instance=access.user).data
|
||||
if not role in models.PRIVILEGED_ROLES:
|
||||
assert response.status_code == 403
|
||||
else:
|
||||
access_user = serializers.UserSerializer(instance=access.user).data
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": str(access.id),
|
||||
"user": access_user,
|
||||
"team": "",
|
||||
"role": access.role,
|
||||
"abilities": access.get_abilities(user),
|
||||
}
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": str(access.id),
|
||||
"user": access_user,
|
||||
"team": "",
|
||||
"role": access.role,
|
||||
"abilities": access.get_abilities(user),
|
||||
}
|
||||
|
||||
|
||||
def test_api_document_accesses_update_anonymous():
|
||||
|
||||
@@ -67,10 +67,12 @@ def test_api_documents_attachment_upload_anonymous_success():
|
||||
file_path = response.json()["file"]
|
||||
match = pattern.search(file_path)
|
||||
file_id = match.group(1)
|
||||
|
||||
# Validate that file_id is a valid UUID
|
||||
uuid.UUID(file_id)
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.attachments == [f"{document.id!s}/attachments/{file_id!s}.png"]
|
||||
|
||||
# Now, check the metadata of the uploaded file
|
||||
key = file_path.replace("/media", "")
|
||||
file_head = default_storage.connection.meta.client.head_object(
|
||||
@@ -112,6 +114,9 @@ def test_api_documents_attachment_upload_authenticated_forbidden(reach, role):
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.attachments == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reach, role",
|
||||
@@ -122,8 +127,8 @@ def test_api_documents_attachment_upload_authenticated_forbidden(reach, role):
|
||||
)
|
||||
def test_api_documents_attachment_upload_authenticated_success(reach, role):
|
||||
"""
|
||||
Autenticated who are not related to a document should be able to upload a file
|
||||
if the link reach and role permit it.
|
||||
Autenticated users who are not related to a document should be able to upload
|
||||
a file when the link reach and role permit it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -145,6 +150,9 @@ def test_api_documents_attachment_upload_authenticated_success(reach, role):
|
||||
# Validate that file_id is a valid UUID
|
||||
uuid.UUID(file_id)
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.attachments == [f"{document.id!s}/attachments/{file_id!s}.png"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_documents_attachment_upload_reader(via, mock_user_teams):
|
||||
@@ -175,6 +183,9 @@ def test_api_documents_attachment_upload_reader(via, mock_user_teams):
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.attachments == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@@ -211,6 +222,9 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams):
|
||||
# Validate that file_id is a valid UUID
|
||||
uuid.UUID(file_id)
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.attachments == [f"{document.id!s}/attachments/{file_id!s}.png"]
|
||||
|
||||
# Now, check the metadata of the uploaded file
|
||||
key = file_path.replace("/media", "")
|
||||
file_head = default_storage.connection.meta.client.head_object(
|
||||
@@ -236,6 +250,9 @@ def test_api_documents_attachment_upload_invalid(client):
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"file": ["No file was submitted."]}
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.attachments == []
|
||||
|
||||
|
||||
def test_api_documents_attachment_upload_size_limit_exceeded(settings):
|
||||
"""The uploaded file should not exceeed the maximum size in settings."""
|
||||
@@ -258,6 +275,9 @@ def test_api_documents_attachment_upload_size_limit_exceeded(settings):
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"file": ["File size exceeds the maximum limit of 1 MB."]}
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.attachments == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,content,extension,content_type",
|
||||
@@ -293,6 +313,11 @@ def test_api_documents_attachment_upload_fix_extension(
|
||||
match = pattern.search(file_path)
|
||||
file_id = match.group(1)
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.attachments == [
|
||||
f"{document.id!s}/attachments/{file_id!s}.{extension:s}"
|
||||
]
|
||||
|
||||
assert "-unsafe" in file_id
|
||||
# Validate that file_id is a valid UUID
|
||||
file_id = file_id.replace("-unsafe", "")
|
||||
@@ -323,6 +348,9 @@ def test_api_documents_attachment_upload_empty_file():
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"file": ["The submitted file is empty."]}
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.attachments == []
|
||||
|
||||
|
||||
def test_api_documents_attachment_upload_unsafe():
|
||||
"""A file with an unsafe mime type should be tagged as such."""
|
||||
@@ -345,6 +373,9 @@ def test_api_documents_attachment_upload_unsafe():
|
||||
match = pattern.search(file_path)
|
||||
file_id = match.group(1)
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.attachments == [f"{document.id!s}/attachments/{file_id!s}.exe"]
|
||||
|
||||
assert "-unsafe" in file_id
|
||||
# Validate that file_id is a valid UUID
|
||||
file_id = file_id.replace("-unsafe", "")
|
||||
|
||||
207
src/backend/core/tests/documents/test_api_documents_duplicate.py
Normal file
207
src/backend/core/tests/documents/test_api_documents_duplicate.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Test file uploads API endpoint for users in impress's core app.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.utils import timezone
|
||||
|
||||
import pycrdt
|
||||
import pytest
|
||||
import requests
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
PIXEL = (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00"
|
||||
b"\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xf8\xff\xff?\x00\x05\xfe\x02\xfe"
|
||||
b"\xa7V\xbd\xfa\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
)
|
||||
|
||||
|
||||
def get_image_refs(document_id):
|
||||
"""Generate an image key for testing."""
|
||||
image_key = f"{document_id!s}/attachments/{uuid.uuid4()!s}.png"
|
||||
default_storage.connection.meta.client.put_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=image_key,
|
||||
Body=BytesIO(PIXEL),
|
||||
ContentType="image/png",
|
||||
)
|
||||
return image_key, f"http://localhost/media/{image_key:s}"
|
||||
|
||||
|
||||
def test_api_documents_duplicate_forbidden():
|
||||
"""A user who doesn't have read access to a document should not be allowed to duplicate it."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(
|
||||
link_reach="restricted",
|
||||
users=[factories.UserFactory()],
|
||||
title="my document",
|
||||
)
|
||||
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/duplicate/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert models.Document.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_documents_duplicate_anonymous():
|
||||
"""Anonymous users should not be able to duplicate documents even with read access."""
|
||||
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
response = APIClient().post(f"/api/v1.0/documents/{document.id!s}/duplicate/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert models.Document.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("index", range(3))
|
||||
def test_api_documents_duplicate_success(index):
|
||||
"""
|
||||
Anonymous users should be able to retrieve attachments linked to a public document.
|
||||
Accesses should not be duplicated if the user does not request it specifically.
|
||||
Attachments that are not in the content should not be passed for access in the
|
||||
duplicated document's "attachments" list.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document_ids = [uuid.uuid4() for _ in range(3)]
|
||||
image_refs = [get_image_refs(doc_id) for doc_id in document_ids]
|
||||
|
||||
# Create document content with the first image only
|
||||
ydoc = pycrdt.Doc()
|
||||
fragment = pycrdt.XmlFragment(
|
||||
[
|
||||
pycrdt.XmlElement("img", {"src": image_refs[0][1]}),
|
||||
]
|
||||
)
|
||||
ydoc["document-store"] = fragment
|
||||
update = ydoc.get_update()
|
||||
base64_content = base64.b64encode(update).decode("utf-8")
|
||||
|
||||
# Create documents
|
||||
document = factories.DocumentFactory(
|
||||
id=document_ids[index],
|
||||
content=base64_content,
|
||||
link_reach="restricted",
|
||||
users=[user, factories.UserFactory()],
|
||||
title="document with an image",
|
||||
attachments=[key for key, _ in image_refs],
|
||||
)
|
||||
factories.DocumentFactory(id=document_ids[(index + 1) % 3])
|
||||
# Don't create document for third ID to check that it doesn't impact access to attachments
|
||||
|
||||
# Duplicate the document via the API endpoint
|
||||
response = client.post(f"/api/v1.0/documents/{document.id}/duplicate/")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
duplicated_document = models.Document.objects.get(id=response.json()["id"])
|
||||
assert duplicated_document.title == "Copy of document with an image"
|
||||
assert duplicated_document.content == document.content
|
||||
assert duplicated_document.creator == user
|
||||
assert duplicated_document.link_reach == "restricted"
|
||||
assert duplicated_document.link_role == "reader"
|
||||
assert duplicated_document.duplicated_from == document
|
||||
assert duplicated_document.attachments == [
|
||||
image_refs[0][0]
|
||||
] # Only the first image key
|
||||
assert duplicated_document.get_parent() == document.get_parent()
|
||||
assert duplicated_document.path == document.get_next_sibling().path
|
||||
|
||||
# Check that accesses were not duplicated.
|
||||
# The user who did the duplicate is forced as owner
|
||||
assert duplicated_document.accesses.count() == 1
|
||||
access = duplicated_document.accesses.first()
|
||||
assert access.user == user
|
||||
assert access.role == "owner"
|
||||
|
||||
# Ensure access persists after the owner loses access to the original document
|
||||
models.DocumentAccess.objects.filter(document=document).delete()
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=image_refs[0][1]
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
authorization = response["Authorization"]
|
||||
assert "AWS4-HMAC-SHA256 Credential=" in authorization
|
||||
assert (
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
response = requests.get(
|
||||
f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{image_refs[0][0]:s}",
|
||||
headers={
|
||||
"authorization": authorization,
|
||||
"x-amz-date": response["x-amz-date"],
|
||||
"x-amz-content-sha256": response["x-amz-content-sha256"],
|
||||
"Host": f"{s3_url.hostname:s}:{s3_url.port:d}",
|
||||
},
|
||||
timeout=1,
|
||||
)
|
||||
assert response.content == PIXEL
|
||||
|
||||
# Ensure the other images are not accessible
|
||||
for _, url in image_refs[1:]:
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=url
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_api_documents_duplicate_with_accesses():
|
||||
"""Accesses should be duplicated if the user requests it specifically."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(
|
||||
users=[user],
|
||||
title="document with accesses",
|
||||
)
|
||||
user_access = factories.UserDocumentAccessFactory(document=document)
|
||||
team_access = factories.TeamDocumentAccessFactory(document=document)
|
||||
|
||||
# Duplicate the document via the API endpoint requesting to duplicate accesses
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/duplicate/",
|
||||
{"with_accesses": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
duplicated_document = models.Document.objects.get(id=response.json()["id"])
|
||||
assert duplicated_document.title == "Copy of document with accesses"
|
||||
assert duplicated_document.content == document.content
|
||||
assert duplicated_document.link_reach == document.link_reach
|
||||
assert duplicated_document.link_role == document.link_role
|
||||
assert duplicated_document.creator == user
|
||||
assert duplicated_document.duplicated_from == document
|
||||
assert duplicated_document.attachments == []
|
||||
|
||||
# Check that accesses were duplicated and the user who did the duplicate is forced as owner
|
||||
duplicated_accesses = duplicated_document.accesses
|
||||
assert duplicated_accesses.count() == 3
|
||||
assert duplicated_accesses.get(user=user).role == "owner"
|
||||
assert duplicated_accesses.get(user=user_access.user).role == user_access.role
|
||||
assert duplicated_accesses.get(team=team_access.team).role == team_access.role
|
||||
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
Test file uploads API endpoint for users in impress's core app.
|
||||
Test media-auth authorization API endpoint in docs core app.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
@@ -14,19 +14,32 @@ import pytest
|
||||
import requests
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core import factories, models
|
||||
from core.tests.conftest import TEAM, USER, VIA
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_documents_media_auth_unkown_document():
|
||||
"""
|
||||
Trying to download a media related to a document ID that does not exist
|
||||
should not have the side effect to create it (no regression test).
|
||||
"""
|
||||
original_url = f"http://localhost/media/{uuid4()!s}/attachments/{uuid4()!s}.jpg"
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert models.Document.objects.exists() is False
|
||||
|
||||
|
||||
def test_api_documents_media_auth_anonymous_public():
|
||||
"""Anonymous users should be able to retrieve attachments linked to a public document"""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
filename = f"{uuid.uuid4()!s}.jpg"
|
||||
key = f"{document.pk!s}/attachments/{filename:s}"
|
||||
|
||||
document_id = uuid4()
|
||||
filename = f"{uuid4()!s}.jpg"
|
||||
key = f"{document_id!s}/attachments/{filename:s}"
|
||||
default_storage.connection.meta.client.put_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=key,
|
||||
@@ -34,6 +47,8 @@ def test_api_documents_media_auth_anonymous_public():
|
||||
ContentType="text/plain",
|
||||
)
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
|
||||
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
@@ -66,8 +81,6 @@ def test_api_documents_media_auth_anonymous_public():
|
||||
|
||||
def test_api_documents_media_auth_extensions():
|
||||
"""Files with extensions of any format should work."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
extensions = [
|
||||
"c",
|
||||
"go",
|
||||
@@ -76,10 +89,15 @@ def test_api_documents_media_auth_extensions():
|
||||
"woff2",
|
||||
"appimage",
|
||||
]
|
||||
document_id = uuid4()
|
||||
keys = []
|
||||
for ext in extensions:
|
||||
filename = f"{uuid.uuid4()!s}.{ext:s}"
|
||||
key = f"{document.pk!s}/attachments/{filename:s}"
|
||||
filename = f"{uuid4()!s}.{ext:s}"
|
||||
keys.append(f"{document_id!s}/attachments/{filename:s}")
|
||||
|
||||
factories.DocumentFactory(link_reach="public", attachments=keys)
|
||||
|
||||
for key in keys:
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
@@ -94,10 +112,11 @@ def test_api_documents_media_auth_anonymous_authenticated_or_restricted(reach):
|
||||
Anonymous users should not be allowed to retrieve attachments linked to a document
|
||||
with link reach set to authenticated or restricted.
|
||||
"""
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
document_id = uuid4()
|
||||
filename = f"{uuid4()!s}.jpg"
|
||||
media_url = f"http://localhost/media/{document_id!s}/attachments/{filename:s}"
|
||||
|
||||
filename = f"{uuid.uuid4()!s}.jpg"
|
||||
media_url = f"http://localhost/media/{document.pk!s}/attachments/{filename:s}"
|
||||
factories.DocumentFactory(id=document_id, link_reach=reach)
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
@@ -107,20 +126,16 @@ def test_api_documents_media_auth_anonymous_authenticated_or_restricted(reach):
|
||||
assert "Authorization" not in response
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||
def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
|
||||
def test_api_documents_media_auth_anonymous_attachments():
|
||||
"""
|
||||
Authenticated users who are not related to a document should be able to retrieve
|
||||
attachments related to a document with public or authenticated link reach.
|
||||
Declaring a media key as original attachment on a document to which
|
||||
a user has access should give them access to the attachment file
|
||||
regardless of their access rights on the original document.
|
||||
"""
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
filename = f"{uuid.uuid4()!s}.jpg"
|
||||
key = f"{document.pk!s}/attachments/{filename:s}"
|
||||
document_id = uuid4()
|
||||
filename = f"{uuid4()!s}.jpg"
|
||||
key = f"{document_id!s}/attachments/{filename:s}"
|
||||
media_url = f"http://localhost/media/{key:s}"
|
||||
|
||||
default_storage.connection.meta.client.put_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
@@ -129,9 +144,73 @@ def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
|
||||
ContentType="text/plain",
|
||||
)
|
||||
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
factories.DocumentFactory(id=document_id, link_reach="restricted")
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
# Let's now add a document to which the anonymous user has access and
|
||||
# pointing to the attachment
|
||||
parent = factories.DocumentFactory(link_reach="public")
|
||||
factories.DocumentFactory(parent=parent, link_reach="restricted", attachments=[key])
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
authorization = response["Authorization"]
|
||||
assert "AWS4-HMAC-SHA256 Credential=" in authorization
|
||||
assert (
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
response = requests.get(
|
||||
file_url,
|
||||
headers={
|
||||
"authorization": authorization,
|
||||
"x-amz-date": response["x-amz-date"],
|
||||
"x-amz-content-sha256": response["x-amz-content-sha256"],
|
||||
"Host": f"{s3_url.hostname:s}:{s3_url.port:d}",
|
||||
},
|
||||
timeout=1,
|
||||
)
|
||||
assert response.content.decode("utf-8") == "my prose"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||
def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
|
||||
"""
|
||||
Authenticated users who are not related to a document should be able to retrieve
|
||||
attachments related to a document with public or authenticated link reach.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document_id = uuid4()
|
||||
filename = f"{uuid4()!s}.jpg"
|
||||
key = f"{document_id!s}/attachments/{filename:s}"
|
||||
media_url = f"http://localhost/media/{key:s}"
|
||||
|
||||
default_storage.connection.meta.client.put_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=key,
|
||||
Body=BytesIO(b"my prose"),
|
||||
ContentType="text/plain",
|
||||
)
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach=reach, attachments=[key])
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -164,14 +243,18 @@ def test_api_documents_media_auth_authenticated_restricted():
|
||||
Authenticated users who are not related to a document should not be allowed to
|
||||
retrieve attachments linked to a document that is restricted.
|
||||
"""
|
||||
document = factories.DocumentFactory(link_reach="restricted")
|
||||
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
filename = f"{uuid.uuid4()!s}.jpg"
|
||||
media_url = f"http://localhost/media/{document.pk!s}/attachments/{filename:s}"
|
||||
document_id = uuid4()
|
||||
filename = f"{uuid4()!s}.jpg"
|
||||
key = f"{document_id!s}/attachments/{filename:s}"
|
||||
media_url = f"http://localhost/media/{key:s}"
|
||||
|
||||
factories.DocumentFactory(
|
||||
id=document_id, link_reach="restricted", attachments=[key]
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
@@ -191,16 +274,10 @@ def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
|
||||
|
||||
filename = f"{uuid.uuid4()!s}.jpg"
|
||||
key = f"{document.pk!s}/attachments/{filename:s}"
|
||||
|
||||
document_id = uuid4()
|
||||
filename = f"{uuid4()!s}.jpg"
|
||||
key = f"{document_id!s}/attachments/{filename:s}"
|
||||
media_url = f"http://localhost/media/{key:s}"
|
||||
default_storage.connection.meta.client.put_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=key,
|
||||
@@ -208,9 +285,17 @@ def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||
ContentType="text/plain",
|
||||
)
|
||||
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
document = factories.DocumentFactory(
|
||||
id=document_id, link_reach="restricted", attachments=[key]
|
||||
)
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -37,6 +37,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"cors_proxy": True,
|
||||
"descendants": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
# Anonymous user can't favorite a document even with read access
|
||||
"favorite": False,
|
||||
"invite_owner": False,
|
||||
@@ -103,6 +104,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
# Anonymous user can't favorite a document even with read access
|
||||
"favorite": False,
|
||||
"invite_owner": False,
|
||||
@@ -198,6 +200,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
@@ -271,6 +274,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
@@ -450,6 +454,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": access.role == "owner",
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": access.role == "owner",
|
||||
"link_configuration": access.role in ["administrator", "owner"],
|
||||
@@ -784,7 +789,7 @@ def test_api_documents_retrieve_user_roles(django_assert_max_num_queries):
|
||||
)
|
||||
expected_roles = {access.role for access in accesses}
|
||||
|
||||
with django_assert_max_num_queries(12):
|
||||
with django_assert_max_num_queries(14):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -81,6 +81,7 @@ def test_api_documents_trashbin_format():
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": True,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": True,
|
||||
"link_configuration": True,
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Test extract-attachments on document update in docs core app.
|
||||
"""
|
||||
|
||||
import base64
|
||||
from uuid import uuid4
|
||||
|
||||
import pycrdt
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def get_ydoc_with_mages(image_keys):
|
||||
"""Return a ydoc from text for testing purposes."""
|
||||
ydoc = pycrdt.Doc()
|
||||
fragment = pycrdt.XmlFragment(
|
||||
[
|
||||
pycrdt.XmlElement("img", {"src": f"http://localhost/media/{key:s}"})
|
||||
for key in image_keys
|
||||
]
|
||||
)
|
||||
ydoc["document-store"] = fragment
|
||||
update = ydoc.get_update()
|
||||
return base64.b64encode(update).decode("utf-8")
|
||||
|
||||
|
||||
def test_api_documents_update_new_attachment_keys_anonymous(django_assert_num_queries):
|
||||
"""
|
||||
When an anonymous user updates a document, the attachment keys extracted from the
|
||||
updated content should be added to the list of "attachments" ot the document if these
|
||||
attachments are already readable by anonymous users.
|
||||
"""
|
||||
image_keys = [f"{uuid4()!s}/attachments/{uuid4()!s}.png" for _ in range(4)]
|
||||
document = factories.DocumentFactory(
|
||||
content=get_ydoc_with_mages(image_keys[:1]),
|
||||
attachments=[image_keys[0]],
|
||||
link_reach="public",
|
||||
link_role="editor",
|
||||
)
|
||||
|
||||
factories.DocumentFactory(attachments=[image_keys[1]], link_reach="public")
|
||||
factories.DocumentFactory(attachments=[image_keys[2]], link_reach="authenticated")
|
||||
factories.DocumentFactory(attachments=[image_keys[3]], link_reach="restricted")
|
||||
expected_keys = {image_keys[i] for i in [0, 1]}
|
||||
|
||||
with django_assert_num_queries(9):
|
||||
response = APIClient().put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages(image_keys)},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
document.refresh_from_db()
|
||||
assert set(document.attachments) == expected_keys
|
||||
|
||||
# Check that the db query to check attachments readability for extracted
|
||||
# keys is not done if the content changes but no new keys are found
|
||||
with django_assert_num_queries(5):
|
||||
response = APIClient().put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages(image_keys[:2])},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
document.refresh_from_db()
|
||||
assert len(document.attachments) == 2
|
||||
assert set(document.attachments) == expected_keys
|
||||
|
||||
|
||||
def test_api_documents_update_new_attachment_keys_authenticated(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
When an authenticated user updates a document, the attachment keys extracted from the
|
||||
updated content should be added to the list of "attachments" ot the document if these
|
||||
attachments are already readable by the editing user.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
image_keys = [f"{uuid4()!s}/attachments/{uuid4()!s}.png" for _ in range(5)]
|
||||
document = factories.DocumentFactory(
|
||||
content=get_ydoc_with_mages(image_keys[:1]),
|
||||
attachments=[image_keys[0]],
|
||||
users=[(user, "editor")],
|
||||
)
|
||||
|
||||
factories.DocumentFactory(attachments=[image_keys[1]], link_reach="public")
|
||||
factories.DocumentFactory(attachments=[image_keys[2]], link_reach="authenticated")
|
||||
factories.DocumentFactory(attachments=[image_keys[3]], link_reach="restricted")
|
||||
factories.DocumentFactory(attachments=[image_keys[4]], users=[user])
|
||||
expected_keys = {image_keys[i] for i in [0, 1, 2, 4]}
|
||||
|
||||
with django_assert_num_queries(10):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages(image_keys)},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
document.refresh_from_db()
|
||||
assert set(document.attachments) == expected_keys
|
||||
|
||||
# Check that the db query to check attachments readability for extracted
|
||||
# keys is not done if the content changes but no new keys are found
|
||||
with django_assert_num_queries(6):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages(image_keys[:2])},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
document.refresh_from_db()
|
||||
assert len(document.attachments) == 4
|
||||
assert set(document.attachments) == expected_keys
|
||||
|
||||
|
||||
def test_api_documents_update_new_attachment_keys_duplicate():
|
||||
"""
|
||||
Duplicate keys in the content should not result in duplicates in the document's attachments.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
image_key1 = f"{uuid4()!s}/attachments/{uuid4()!s}.png"
|
||||
image_key2 = f"{uuid4()!s}/attachments/{uuid4()!s}.png"
|
||||
document = factories.DocumentFactory(
|
||||
content=get_ydoc_with_mages([image_key1]),
|
||||
attachments=[image_key1],
|
||||
users=[(user, "editor")],
|
||||
)
|
||||
|
||||
factories.DocumentFactory(attachments=[image_key2], users=[user])
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages([image_key1, image_key2, image_key2])},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
document.refresh_from_db()
|
||||
assert len(document.attachments) == 2
|
||||
assert set(document.attachments) == {image_key1, image_key2}
|
||||
@@ -1,35 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from core import factories
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_blank_title_migration(migrator):
|
||||
"""
|
||||
Test that the migration fixes the titles of documents that are
|
||||
"Untitled document", "Unbenanntes Dokument" or "Document sans titre"
|
||||
"""
|
||||
migrator.apply_initial_migration(("core", "0017_add_fields_for_soft_delete"))
|
||||
|
||||
english_doc = factories.DocumentFactory(title="Untitled document")
|
||||
german_doc = factories.DocumentFactory(title="Unbenanntes Dokument")
|
||||
french_doc = factories.DocumentFactory(title="Document sans titre")
|
||||
other_doc = factories.DocumentFactory(title="My document")
|
||||
|
||||
assert english_doc.title == "Untitled document"
|
||||
assert german_doc.title == "Unbenanntes Dokument"
|
||||
assert french_doc.title == "Document sans titre"
|
||||
assert other_doc.title == "My document"
|
||||
|
||||
# Apply the migration
|
||||
migrator.apply_tested_migration(("core", "0018_update_blank_title"))
|
||||
|
||||
english_doc.refresh_from_db()
|
||||
german_doc.refresh_from_db()
|
||||
french_doc.refresh_from_db()
|
||||
other_doc.refresh_from_db()
|
||||
|
||||
assert english_doc.title == None
|
||||
assert german_doc.title == None
|
||||
assert french_doc.title == None
|
||||
assert other_doc.title == "My document"
|
||||
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
|
||||
from core import models
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_blank_title_migration(migrator):
|
||||
"""
|
||||
Test that the migration fixes the titles of documents that are
|
||||
"Untitled document", "Unbenanntes Dokument" or "Document sans titre"
|
||||
"""
|
||||
old_state = migrator.apply_initial_migration(
|
||||
("core", "0017_add_fields_for_soft_delete")
|
||||
)
|
||||
OldDocument = old_state.apps.get_model("core", "Document")
|
||||
|
||||
old_english_doc = OldDocument.objects.create(
|
||||
title="Untitled document", depth=1, path="0000001"
|
||||
)
|
||||
old_german_doc = OldDocument.objects.create(
|
||||
title="Unbenanntes Dokument", depth=1, path="0000002"
|
||||
)
|
||||
old_french_doc = OldDocument.objects.create(
|
||||
title="Document sans titre", depth=1, path="0000003"
|
||||
)
|
||||
old_other_doc = OldDocument.objects.create(
|
||||
title="My document", depth=1, path="0000004"
|
||||
)
|
||||
|
||||
assert old_english_doc.title == "Untitled document"
|
||||
assert old_german_doc.title == "Unbenanntes Dokument"
|
||||
assert old_french_doc.title == "Document sans titre"
|
||||
assert old_other_doc.title == "My document"
|
||||
|
||||
# Apply the migration
|
||||
new_state = migrator.apply_tested_migration(("core", "0018_update_blank_title"))
|
||||
NewDocument = new_state.apps.get_model("core", "Document")
|
||||
|
||||
new_english_doc = NewDocument.objects.get(pk=old_english_doc.pk)
|
||||
new_german_doc = NewDocument.objects.get(pk=old_german_doc.pk)
|
||||
new_french_doc = NewDocument.objects.get(pk=old_french_doc.pk)
|
||||
new_other_doc = NewDocument.objects.get(pk=old_other_doc.pk)
|
||||
|
||||
assert new_english_doc.title == None
|
||||
assert new_german_doc.title == None
|
||||
assert new_french_doc.title == None
|
||||
assert new_other_doc.title == "My document"
|
||||
@@ -0,0 +1,54 @@
|
||||
import base64
|
||||
import uuid
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
import pycrdt
|
||||
import pytest
|
||||
|
||||
from core import models
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_populate_attachments_on_all_documents(migrator):
|
||||
"""Test that the migration populates attachments on existing documents."""
|
||||
old_state = migrator.apply_initial_migration(
|
||||
("core", "0019_alter_user_language_default_to_null")
|
||||
)
|
||||
OldDocument = old_state.apps.get_model("core", "Document")
|
||||
|
||||
old_doc_without_attachments = OldDocument.objects.create(
|
||||
title="Doc without attachments", depth=1, path="0000002"
|
||||
)
|
||||
old_doc_with_attachments = OldDocument.objects.create(
|
||||
title="Doc with attachments", depth=1, path="0000001"
|
||||
)
|
||||
|
||||
# Create document content with an image
|
||||
file_key = f"{old_doc_with_attachments.id!s}/file"
|
||||
image_key = f"{old_doc_with_attachments.id!s}/attachments/{uuid.uuid4()!s}.png"
|
||||
ydoc = pycrdt.Doc()
|
||||
fragment = pycrdt.XmlFragment(
|
||||
[pycrdt.XmlElement("img", {"src": f"http://localhost/media/{image_key:s}"})]
|
||||
)
|
||||
ydoc["document-store"] = fragment
|
||||
update = ydoc.get_update()
|
||||
base64_content = base64.b64encode(update).decode("utf-8")
|
||||
bytes_content = base64_content.encode("utf-8")
|
||||
content_file = ContentFile(bytes_content)
|
||||
default_storage.save(file_key, content_file)
|
||||
|
||||
# Apply the migration
|
||||
new_state = migrator.apply_tested_migration(
|
||||
("core", "0020_remove_is_public_add_field_attachments_and_duplicated_from")
|
||||
)
|
||||
NewDocument = new_state.apps.get_model("core", "Document")
|
||||
|
||||
new_doc_with_attachments = NewDocument.objects.get(pk=old_doc_with_attachments.pk)
|
||||
new_doc_without_attachments = NewDocument.objects.get(
|
||||
pk=old_doc_without_attachments.pk
|
||||
)
|
||||
|
||||
assert new_doc_without_attachments.attachments == []
|
||||
assert new_doc_with_attachments.attachments == [image_key]
|
||||
@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories
|
||||
from core import factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -294,7 +294,7 @@ def test_models_document_access_get_abilities_for_editor_of_owner():
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
@@ -311,7 +311,7 @@ def test_models_document_access_get_abilities_for_editor_of_administrator():
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
@@ -333,7 +333,7 @@ def test_models_document_access_get_abilities_for_editor_of_editor_user(
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
@@ -353,7 +353,7 @@ def test_models_document_access_get_abilities_for_reader_of_owner():
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
@@ -370,7 +370,7 @@ def test_models_document_access_get_abilities_for_reader_of_administrator():
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
@@ -392,7 +392,7 @@ def test_models_document_access_get_abilities_for_reader_of_reader_user(
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
@@ -412,8 +412,16 @@ def test_models_document_access_get_abilities_preset_role(django_assert_num_quer
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", models.RoleChoices)
|
||||
def test_models_document_access_get_abilities_retrieve_own_access(role):
|
||||
"""Check abilities of self access for the owner of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role=role)
|
||||
abilities = access.get_abilities(access.user)
|
||||
assert abilities["retrieve"] is True
|
||||
|
||||
@@ -161,6 +161,7 @@ def test_models_documents_get_abilities_forbidden(
|
||||
"descendants": False,
|
||||
"cors_proxy": False,
|
||||
"destroy": False,
|
||||
"duplicate": False,
|
||||
"favorite": False,
|
||||
"invite_owner": False,
|
||||
"media_auth": False,
|
||||
@@ -220,6 +221,7 @@ def test_models_documents_get_abilities_reader(
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": is_authenticated,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
@@ -281,6 +283,7 @@ def test_models_documents_get_abilities_editor(
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": is_authenticated,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
@@ -331,6 +334,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": True,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": True,
|
||||
"link_configuration": True,
|
||||
@@ -378,6 +382,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": False,
|
||||
"link_configuration": True,
|
||||
@@ -428,6 +433,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
@@ -485,6 +491,7 @@ def test_models_documents_get_abilities_reader_user(
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
@@ -540,6 +547,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
@@ -1297,3 +1305,47 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
def test_models_documents_get_select_options(ancestors_links, select_options):
|
||||
"""Validate that the "get_select_options" method operates as expected."""
|
||||
assert models.LinkReachChoices.get_select_options(ancestors_links) == select_options
|
||||
|
||||
|
||||
def test_models_documents_compute_ancestors_links_no_highest_readable():
|
||||
"""Test the compute_ancestors_links method."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
assert document.compute_ancestors_links(user=AnonymousUser()) == []
|
||||
|
||||
|
||||
def test_models_documents_compute_ancestors_links_highest_readable(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""Test the compute_ancestors_links method."""
|
||||
user = factories.UserFactory()
|
||||
other_user = factories.UserFactory()
|
||||
root = factories.DocumentFactory(
|
||||
link_reach="restricted", link_role="reader", users=[user]
|
||||
)
|
||||
|
||||
factories.DocumentFactory(
|
||||
parent=root, link_reach="public", link_role="reader", users=[user]
|
||||
)
|
||||
child2 = factories.DocumentFactory(
|
||||
parent=root,
|
||||
link_reach="authenticated",
|
||||
link_role="editor",
|
||||
users=[user, other_user],
|
||||
)
|
||||
child3 = factories.DocumentFactory(
|
||||
parent=child2,
|
||||
link_reach="authenticated",
|
||||
link_role="reader",
|
||||
users=[user, other_user],
|
||||
)
|
||||
|
||||
with django_assert_num_queries(2):
|
||||
assert child3.compute_ancestors_links(user=user) == [
|
||||
{"link_reach": root.link_reach, "link_role": root.link_role},
|
||||
{"link_reach": child2.link_reach, "link_role": child2.link_role},
|
||||
]
|
||||
|
||||
with django_assert_num_queries(2):
|
||||
assert child3.compute_ancestors_links(user=other_user) == [
|
||||
{"link_reach": child2.link_reach, "link_role": child2.link_role},
|
||||
]
|
||||
|
||||
77
src/backend/core/tests/test_utils.py
Normal file
77
src/backend/core/tests/test_utils.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Test util base64_yjs_to_text."""
|
||||
|
||||
import base64
|
||||
import uuid
|
||||
|
||||
import pycrdt
|
||||
|
||||
from core import utils
|
||||
|
||||
# This base64 string is an example of what is saved in the database.
|
||||
# This base64 is generated from the blocknote editor, it contains
|
||||
# the text \n# *Hello* \n- w**or**ld
|
||||
TEST_BASE64_STRING = (
|
||||
"AR717vLVDgAHAQ5kb2N1bWVudC1zdG9yZQMKYmxvY2tHcm91cAcA9e7y1Q4AAw5ibG9ja0NvbnRh"
|
||||
"aW5lcgcA9e7y1Q4BAwdoZWFkaW5nBwD17vLVDgIGBgD17vLVDgMGaXRhbGljAnt9hPXu8tUOBAVI"
|
||||
"ZWxsb4b17vLVDgkGaXRhbGljBG51bGwoAPXu8tUOAg10ZXh0QWxpZ25tZW50AXcEbGVmdCgA9e7y"
|
||||
"1Q4CBWxldmVsAX0BKAD17vLVDgECaWQBdyQwNGQ2MjM0MS04MzI2LTQyMzYtYTA4My00ODdlMjZm"
|
||||
"YWQyMzAoAPXu8tUOAQl0ZXh0Q29sb3IBdwdkZWZhdWx0KAD17vLVDgEPYmFja2dyb3VuZENvbG9y"
|
||||
"AXcHZGVmYXVsdIf17vLVDgEDDmJsb2NrQ29udGFpbmVyBwD17vLVDhADDmJ1bGxldExpc3RJdGVt"
|
||||
"BwD17vLVDhEGBAD17vLVDhIBd4b17vLVDhMEYm9sZAJ7fYT17vLVDhQCb3KG9e7y1Q4WBGJvbGQE"
|
||||
"bnVsbIT17vLVDhcCbGQoAPXu8tUOEQ10ZXh0QWxpZ25tZW50AXcEbGVmdCgA9e7y1Q4QAmlkAXck"
|
||||
"ZDM1MWUwNjgtM2U1NS00MjI2LThlYTUtYWJiMjYzMTk4ZTJhKAD17vLVDhAJdGV4dENvbG9yAXcH"
|
||||
"ZGVmYXVsdCgA9e7y1Q4QD2JhY2tncm91bmRDb2xvcgF3B2RlZmF1bHSH9e7y1Q4QAw5ibG9ja0Nv"
|
||||
"bnRhaW5lcgcA9e7y1Q4eAwlwYXJhZ3JhcGgoAPXu8tUOHw10ZXh0QWxpZ25tZW50AXcEbGVmdCgA"
|
||||
"9e7y1Q4eAmlkAXckODk3MDBjMDctZTBlMS00ZmUwLWFjYTItODQ5MzIwOWE3ZTQyKAD17vLVDh4J"
|
||||
"dGV4dENvbG9yAXcHZGVmYXVsdCgA9e7y1Q4eD2JhY2tncm91bmRDb2xvcgF3B2RlZmF1bHQA"
|
||||
)
|
||||
|
||||
|
||||
def test_utils_base64_yjs_to_text():
|
||||
"""Test extract text from saved yjs document"""
|
||||
assert utils.base64_yjs_to_text(TEST_BASE64_STRING) == "Hello w or ld"
|
||||
|
||||
|
||||
def test_utils_base64_yjs_to_xml():
|
||||
"""Test extract xml from saved yjs document"""
|
||||
content = utils.base64_yjs_to_xml(TEST_BASE64_STRING)
|
||||
assert (
|
||||
'<heading textAlignment="left" level="1"><italic>Hello</italic></heading>'
|
||||
in content
|
||||
or '<heading level="1" textAlignment="left"><italic>Hello</italic></heading>'
|
||||
in content
|
||||
)
|
||||
assert (
|
||||
'<bulletListItem textAlignment="left">w<bold>or</bold>ld</bulletListItem>'
|
||||
in content
|
||||
)
|
||||
|
||||
|
||||
def test_utils_extract_attachments():
|
||||
"""
|
||||
All attachment keys in the document content should be extracted.
|
||||
"""
|
||||
document_id = uuid.uuid4()
|
||||
image_key1 = f"{document_id!s}/attachments/{uuid.uuid4()!s}.png"
|
||||
image_url1 = f"http://localhost/media/{image_key1:s}"
|
||||
|
||||
image_key2 = f"{uuid.uuid4()!s}/attachments/{uuid.uuid4()!s}.png"
|
||||
image_url2 = f"http://localhost/{image_key2:s}"
|
||||
|
||||
image_key3 = f"{uuid.uuid4()!s}/attachments/{uuid.uuid4()!s}.png"
|
||||
image_url3 = f"http://localhost/media/{image_key3:s}"
|
||||
|
||||
ydoc = pycrdt.Doc()
|
||||
frag = pycrdt.XmlFragment(
|
||||
[
|
||||
pycrdt.XmlElement("img", {"src": image_url1}),
|
||||
pycrdt.XmlElement("img", {"src": image_url2}),
|
||||
pycrdt.XmlElement("p", {}, [pycrdt.XmlText(image_url3)]),
|
||||
]
|
||||
)
|
||||
ydoc["document-store"] = frag
|
||||
|
||||
update = ydoc.get_update()
|
||||
base64_string = base64.b64encode(update).decode("utf-8")
|
||||
# image_key2 is missing the "/media/" part and shouldn't get extracted
|
||||
assert utils.extract_attachments(base64_string) == [image_key1, image_key3]
|
||||
163
src/backend/core/tests/test_utils_filter_descendants.py
Normal file
163
src/backend/core/tests/test_utils_filter_descendants.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Unit tests for the filter_root_paths utility function.
|
||||
"""
|
||||
|
||||
from core.utils import filter_descendants
|
||||
|
||||
|
||||
def test_utils_filter_descendants_success():
|
||||
"""
|
||||
The `filter_descendants` function should correctly identify descendant paths
|
||||
from a given list of paths and root paths.
|
||||
|
||||
This test verifies that the function returns only the paths that have a prefix
|
||||
matching one of the root paths.
|
||||
"""
|
||||
paths = [
|
||||
"0001",
|
||||
"00010001",
|
||||
"000100010001",
|
||||
"000100010002",
|
||||
"000100020001",
|
||||
"000100020002",
|
||||
"0002",
|
||||
"00020001",
|
||||
"00020002",
|
||||
"00030001",
|
||||
"000300010001",
|
||||
"00030002",
|
||||
"0004",
|
||||
"000400010003",
|
||||
"0004000100030001",
|
||||
"000400010004",
|
||||
]
|
||||
root_paths = [
|
||||
"0001",
|
||||
"0002",
|
||||
"000400010003",
|
||||
]
|
||||
filtered_paths = filter_descendants(paths, root_paths, skip_sorting=True)
|
||||
assert filtered_paths == [
|
||||
"0001",
|
||||
"00010001",
|
||||
"000100010001",
|
||||
"000100010002",
|
||||
"000100020001",
|
||||
"000100020002",
|
||||
"0002",
|
||||
"00020001",
|
||||
"00020002",
|
||||
"000400010003",
|
||||
"0004000100030001",
|
||||
]
|
||||
|
||||
|
||||
def test_utils_filter_descendants_sorting():
|
||||
"""
|
||||
The `filter_descendants` function should handle unsorted input when sorting is enabled.
|
||||
|
||||
This test verifies that the function sorts the input if sorting is not skipped
|
||||
and still correctly identifies accessible descendant paths.
|
||||
"""
|
||||
paths = [
|
||||
"000300010001",
|
||||
"000100010002",
|
||||
"0001",
|
||||
"00010001",
|
||||
"000100010001",
|
||||
"000100020002",
|
||||
"000100020001",
|
||||
"0002",
|
||||
"00020001",
|
||||
"00020002",
|
||||
"00030001",
|
||||
"00030002",
|
||||
"0004000100030001",
|
||||
"0004",
|
||||
"000400010003",
|
||||
"000400010004",
|
||||
]
|
||||
root_paths = [
|
||||
"0002",
|
||||
"000400010003",
|
||||
"0001",
|
||||
]
|
||||
filtered_paths = filter_descendants(paths, root_paths)
|
||||
assert filtered_paths == [
|
||||
"0001",
|
||||
"00010001",
|
||||
"000100010001",
|
||||
"000100010002",
|
||||
"000100020001",
|
||||
"000100020002",
|
||||
"0002",
|
||||
"00020001",
|
||||
"00020002",
|
||||
"000400010003",
|
||||
"0004000100030001",
|
||||
]
|
||||
|
||||
filtered_paths = filter_descendants(paths, root_paths, skip_sorting=True)
|
||||
assert filtered_paths == [
|
||||
"0001",
|
||||
"00010001",
|
||||
"000100010001",
|
||||
"000100010002",
|
||||
"000100020001",
|
||||
"000100020002",
|
||||
"0002",
|
||||
"00020001",
|
||||
"00020002",
|
||||
"000400010003",
|
||||
"0004000100030001",
|
||||
]
|
||||
|
||||
|
||||
def test_utils_filter_descendants_empty():
|
||||
"""
|
||||
The function should return an empty list if one or both inputs are empty.
|
||||
"""
|
||||
assert not filter_descendants([], ["0001"])
|
||||
assert not filter_descendants(["0001"], [])
|
||||
assert not filter_descendants([], [])
|
||||
|
||||
|
||||
def test_utils_filter_descendants_no_match():
|
||||
"""
|
||||
The function should return an empty list if no path starts with any root path.
|
||||
"""
|
||||
paths = ["0001", "0002", "0003"]
|
||||
root_paths = ["0004", "0005"]
|
||||
assert not filter_descendants(paths, root_paths, skip_sorting=True)
|
||||
|
||||
|
||||
def test_utils_filter_descendants_exact_match():
|
||||
"""
|
||||
The function should include paths that exactly match a root path.
|
||||
"""
|
||||
paths = ["0001", "0002", "0003"]
|
||||
root_paths = ["0001", "0002"]
|
||||
assert filter_descendants(paths, root_paths, skip_sorting=True) == ["0001", "0002"]
|
||||
|
||||
|
||||
def test_utils_filter_descendants_single_root_matches_all():
|
||||
"""
|
||||
A single root path should match all its descendants.
|
||||
"""
|
||||
paths = ["0001", "00010001", "000100010001", "00010002"]
|
||||
root_paths = ["0001"]
|
||||
assert filter_descendants(paths, root_paths) == [
|
||||
"0001",
|
||||
"00010001",
|
||||
"000100010001",
|
||||
"00010002",
|
||||
]
|
||||
|
||||
|
||||
def test_utils_filter_descendants_path_shorter_than_root():
|
||||
"""
|
||||
A path shorter than any root path should not match.
|
||||
"""
|
||||
paths = ["0001", "0002"]
|
||||
root_paths = ["00010001"]
|
||||
assert not filter_descendants(paths, root_paths)
|
||||
76
src/backend/core/utils.py
Normal file
76
src/backend/core/utils.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Utils for the core app."""
|
||||
|
||||
import base64
|
||||
import re
|
||||
|
||||
import pycrdt
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from core import enums
|
||||
|
||||
|
||||
def filter_descendants(paths, root_paths, skip_sorting=False):
|
||||
"""
|
||||
Filters paths to keep only those that are descendants of any path in root_paths.
|
||||
|
||||
A path is considered a descendant of a root path if it starts with the root path.
|
||||
If `skip_sorting` is not set to True, the function will sort both lists before
|
||||
processing because both `paths` and `root_paths` need to be in lexicographic order
|
||||
before going through the algorithm.
|
||||
|
||||
Args:
|
||||
paths (iterable of str): List of paths to be filtered.
|
||||
root_paths (iterable of str): List of paths to check as potential prefixes.
|
||||
skip_sorting (bool): If True, assumes both `paths` and `root_paths` are already sorted.
|
||||
|
||||
Returns:
|
||||
list of str: A list of sorted paths that are descendants of any path in `root_paths`.
|
||||
"""
|
||||
results = []
|
||||
i = 0
|
||||
n = len(root_paths)
|
||||
|
||||
if not skip_sorting:
|
||||
paths.sort()
|
||||
root_paths.sort()
|
||||
|
||||
for path in paths:
|
||||
# Try to find a matching prefix in the sorted accessible paths
|
||||
while i < n:
|
||||
if path.startswith(root_paths[i]):
|
||||
results.append(path)
|
||||
break
|
||||
if root_paths[i] < path:
|
||||
i += 1
|
||||
else:
|
||||
# If paths[i] > path, no need to keep searching
|
||||
break
|
||||
return results
|
||||
|
||||
|
||||
def base64_yjs_to_xml(base64_string):
|
||||
"""Extract xml from base64 yjs document."""
|
||||
|
||||
decoded_bytes = base64.b64decode(base64_string)
|
||||
# uint8_array = bytearray(decoded_bytes)
|
||||
|
||||
doc = pycrdt.Doc()
|
||||
doc.apply_update(decoded_bytes)
|
||||
return str(doc.get("document-store", type=pycrdt.XmlFragment))
|
||||
|
||||
|
||||
def base64_yjs_to_text(base64_string):
|
||||
"""Extract text from base64 yjs document."""
|
||||
|
||||
blocknote_structure = base64_yjs_to_xml(base64_string)
|
||||
soup = BeautifulSoup(blocknote_structure, "lxml-xml")
|
||||
return soup.get_text(separator=" ", strip=True)
|
||||
|
||||
|
||||
def extract_attachments(content):
|
||||
"""Helper method to extract media paths from a document's content."""
|
||||
if not content:
|
||||
return []
|
||||
|
||||
xml_content = base64_yjs_to_xml(content)
|
||||
return re.findall(enums.MEDIA_STORAGE_URL_EXTRACT, xml_content)
|
||||
@@ -586,14 +586,16 @@ class Base(Configuration):
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "{asctime} {name} {levelname} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": values.Value(
|
||||
"ERROR",
|
||||
environ_name="LOGGING_LEVEL_HANDLERS_CONSOLE",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"formatter": "simple",
|
||||
},
|
||||
},
|
||||
# Override root logger to send it to console
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "2.6.0"
|
||||
version = "3.0.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,7 +25,8 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"boto3==1.37.5",
|
||||
"beautifulsoup4==4.12.3",
|
||||
"boto3==1.37.18",
|
||||
"Brotli==1.1.0",
|
||||
"celery[redis]==5.4.0",
|
||||
"django-configurations==2.5.1",
|
||||
@@ -46,17 +47,19 @@ dependencies = [
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.23.0",
|
||||
"lxml==5.3.1",
|
||||
"markdown==3.7",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"openai==1.65.2",
|
||||
"psycopg[binary]==3.2.5",
|
||||
"openai==1.68.2",
|
||||
"psycopg[binary]==3.2.6",
|
||||
"pycrdt==0.12.10",
|
||||
"PyJWT==2.10.1",
|
||||
"python-magic==0.4.27",
|
||||
"requests==2.32.3",
|
||||
"sentry-sdk==2.22.0",
|
||||
"sentry-sdk==2.24.0",
|
||||
"url-normalize==1.4.3",
|
||||
"whitenoise==6.9.0",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -72,18 +75,18 @@ dev = [
|
||||
"drf-spectacular-sidecar==2025.3.1",
|
||||
"freezegun==1.5.1",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.0.1",
|
||||
"pyfakefs==5.7.4",
|
||||
"ipython==9.0.2",
|
||||
"pyfakefs==5.8.0",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.4",
|
||||
"pylint==3.3.6",
|
||||
"pytest-cov==6.0.0",
|
||||
"pytest-django==4.10.0",
|
||||
"pytest==8.3.5",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.6.1",
|
||||
"responses==0.25.6",
|
||||
"ruff==0.9.9",
|
||||
"types-requests==2.32.0.20250301",
|
||||
"responses==0.25.7",
|
||||
"ruff==0.11.2",
|
||||
"types-requests==2.32.0.20250306",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
|
||||
@@ -6,9 +6,9 @@ import { createDoc, verifyDocName } from './common';
|
||||
|
||||
const config = {
|
||||
CRISP_WEBSITE_ID: null,
|
||||
COLLABORATION_WS_URL: 'ws://localhost:8083/collaboration/ws/',
|
||||
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
|
||||
ENVIRONMENT: 'development',
|
||||
FRONTEND_THEME: 'dsfr',
|
||||
FRONTEND_THEME: 'default',
|
||||
MEDIA_BASE_URL: 'http://localhost:8083',
|
||||
LANGUAGES: [
|
||||
['en-us', 'English'],
|
||||
@@ -99,7 +99,7 @@ test.describe('Config', () => {
|
||||
browserName,
|
||||
}) => {
|
||||
const webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
|
||||
return webSocket.url().includes('ws://localhost:8083/collaboration/ws/');
|
||||
return webSocket.url().includes('ws://localhost:4444/collaboration/ws/');
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
@@ -114,7 +114,7 @@ test.describe('Config', () => {
|
||||
await verifyDocName(page, randomDoc[0]);
|
||||
|
||||
const webSocket = await webSocketPromise;
|
||||
expect(webSocket.url()).toContain('ws://localhost:8083/collaboration/ws/');
|
||||
expect(webSocket.url()).toContain('ws://localhost:4444/collaboration/ws/');
|
||||
});
|
||||
|
||||
test('it checks that Crisp is trying to init from config endpoint', async ({
|
||||
@@ -159,7 +159,7 @@ test.describe('Config: Not loggued', () => {
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const jsonResponse = await response.json();
|
||||
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('dsfr');
|
||||
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('default');
|
||||
|
||||
const footer = page.locator('footer').first();
|
||||
// alt 'Gouvernement Logo' comes from the theme
|
||||
|
||||
@@ -65,7 +65,7 @@ test.describe('Doc Editor', () => {
|
||||
let webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
|
||||
return webSocket
|
||||
.url()
|
||||
.includes('ws://localhost:8083/collaboration/ws/?room=');
|
||||
.includes('ws://localhost:4444/collaboration/ws/?room=');
|
||||
});
|
||||
|
||||
const randomDoc = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
@@ -73,7 +73,7 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
let webSocket = await webSocketPromise;
|
||||
expect(webSocket.url()).toContain(
|
||||
'ws://localhost:8083/collaboration/ws/?room=',
|
||||
'ws://localhost:4444/collaboration/ws/?room=',
|
||||
);
|
||||
|
||||
// Is connected
|
||||
@@ -103,7 +103,7 @@ test.describe('Doc Editor', () => {
|
||||
webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
|
||||
return webSocket
|
||||
.url()
|
||||
.includes('ws://localhost:8083/collaboration/ws/?room=');
|
||||
.includes('ws://localhost:4444/collaboration/ws/?room=');
|
||||
});
|
||||
|
||||
webSocket = await webSocketPromise;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "2.6.0",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
|
||||
@@ -1,492 +1,55 @@
|
||||
const config = {
|
||||
themes: {
|
||||
default: {
|
||||
theme: {
|
||||
colors: {
|
||||
'card-border': '#ededed',
|
||||
'primary-bg': '#FAFAFA',
|
||||
'primary-action': '#1212FF',
|
||||
'primary-050': '#F5F5FE',
|
||||
'primary-100': '#EDF5FA',
|
||||
'primary-150': '#E5EEFA',
|
||||
'primary-950': '#1B1B35',
|
||||
'info-150': '#E5EEFA',
|
||||
'greyscale-000': '#fff',
|
||||
'greyscale-1000': '#161616',
|
||||
'blue-400': '#7AB1E8',
|
||||
'blue-500': '#417DC4',
|
||||
'blue-600': '#3558A2',
|
||||
'brown-400': '#E6BE92',
|
||||
'brown-500': '#BD987A',
|
||||
'brown-600': '#745B47',
|
||||
'cyan-400': '#34BAB5',
|
||||
'cyan-500': '#009099',
|
||||
'cyan-600': '#006A6F',
|
||||
'gold-400': '#FFCA00',
|
||||
'gold-500': '#C3992A',
|
||||
'gold-600': '#695240',
|
||||
'green-400': '#34CB6A',
|
||||
'green-500': '#00A95F',
|
||||
'green-600': '#297254',
|
||||
'olive-400': '#99C221',
|
||||
'olive-500': '#68A532',
|
||||
'olive-600': '#447049',
|
||||
'orange-400': '#FF732C',
|
||||
'orange-500': '#E4794A',
|
||||
'orange-600': '#755348',
|
||||
'pink-400': '#FFB7AE',
|
||||
'pink-500': '#E18B76',
|
||||
'pink-600': '#8D533E',
|
||||
'purple-400': '#CE70CC',
|
||||
'purple-500': '#A558A0',
|
||||
'purple-600': '#6E445A',
|
||||
'yellow-400': '#D8C634',
|
||||
'yellow-500': '#B7A73F',
|
||||
'yellow-600': '#66673D',
|
||||
},
|
||||
font: {
|
||||
sizes: {
|
||||
xs: '0.75rem',
|
||||
sm: '0.875rem',
|
||||
md: '1rem',
|
||||
lg: '1.125rem',
|
||||
ml: '0.938rem',
|
||||
xl: '1.25rem',
|
||||
t: '0.6875rem',
|
||||
s: '0.75rem',
|
||||
h1: '2rem',
|
||||
h2: '1.75rem',
|
||||
h3: '1.5rem',
|
||||
h4: '1.375rem',
|
||||
h5: '1.25rem',
|
||||
h6: '1.125rem',
|
||||
'xl-alt': '5rem',
|
||||
'lg-alt': '4.5rem',
|
||||
'md-alt': '4rem',
|
||||
'sm-alt': '3.5rem',
|
||||
'xs-alt': '3rem',
|
||||
},
|
||||
weights: {
|
||||
thin: 100,
|
||||
extrabold: 800,
|
||||
black: 900,
|
||||
},
|
||||
},
|
||||
spacings: {
|
||||
'0': '0',
|
||||
none: '0',
|
||||
auto: 'auto',
|
||||
bx: '2.2rem',
|
||||
full: '100%',
|
||||
'4xs': '0.125rem',
|
||||
'3xs': '0.25rem',
|
||||
'2xs': '0.375rem',
|
||||
xs: '0.5rem',
|
||||
sm: '0.75rem',
|
||||
base: '1rem',
|
||||
md: '1.5rem',
|
||||
lg: '2rem',
|
||||
xl: '2.5rem',
|
||||
xxl: '3rem',
|
||||
xxxl: '3.5rem',
|
||||
'4xl': '4rem',
|
||||
'5xl': '4.5rem',
|
||||
'6xl': '6rem',
|
||||
'7xl': '7.5rem',
|
||||
},
|
||||
breakpoints: {
|
||||
xxs: '320px',
|
||||
xs: '480px',
|
||||
},
|
||||
logo: {
|
||||
src: '',
|
||||
widthHeader: '',
|
||||
widthFooter: '',
|
||||
alt: '',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
datagrid: {
|
||||
header: {
|
||||
weight: 'var(--c--theme--font--weights--extrabold)',
|
||||
size: 'var(--c--theme--font--sizes--ml)',
|
||||
},
|
||||
cell: {
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
size: 'var(--c--theme--font--sizes--ml)',
|
||||
},
|
||||
},
|
||||
'forms-checkbox': {
|
||||
'background-color': {
|
||||
hover: '#055fd214',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
'font-size': 'var(--c--theme--font--sizes--ml)',
|
||||
},
|
||||
'forms-datepicker': {
|
||||
'border-color': 'var(--c--theme--colors--primary-500)',
|
||||
'value-color': 'var(--c--theme--colors--primary-500)',
|
||||
'border-radius': {
|
||||
hover: 'var(--c--components--forms-datepicker--border-radius)',
|
||||
focus: 'var(--c--components--forms-datepicker--border-radius)',
|
||||
},
|
||||
},
|
||||
'forms-field': {
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
'value-color': 'var(--c--theme--colors--primary-500)',
|
||||
width: 'auto',
|
||||
},
|
||||
'forms-input': {
|
||||
'value-color': 'var(--c--theme--colors--primary-500)',
|
||||
'border-color': 'var(--c--theme--colors--primary-500)',
|
||||
color: {
|
||||
error: 'var(--c--theme--colors--danger-500)',
|
||||
'error-hover': 'var(--c--theme--colors--danger-500)',
|
||||
'box-shadow-error-hover': 'var(--c--theme--colors--danger-500)',
|
||||
},
|
||||
},
|
||||
'forms-labelledbox': {
|
||||
'label-color': {
|
||||
small: 'var(--c--theme--colors--primary-500)',
|
||||
'small-disabled': 'var(--c--theme--colors--greyscale-400)',
|
||||
big: {
|
||||
disabled: 'var(--c--theme--colors--greyscale-400)',
|
||||
},
|
||||
},
|
||||
},
|
||||
'forms-select': {
|
||||
'border-color': 'var(--c--theme--colors--primary-500)',
|
||||
'border-color-disabled-hover':
|
||||
'var(--c--theme--colors--greyscale-200)',
|
||||
'border-radius': {
|
||||
hover: 'var(--c--components--forms-select--border-radius)',
|
||||
focus: 'var(--c--components--forms-select--border-radius)',
|
||||
},
|
||||
'font-size': 'var(--c--theme--font--sizes--ml)',
|
||||
'menu-background-color': '#fff',
|
||||
'item-background-color': {
|
||||
hover: 'var(--c--theme--colors--primary-300)',
|
||||
},
|
||||
},
|
||||
'forms-switch': {
|
||||
'accent-color': 'var(--c--theme--colors--primary-400)',
|
||||
},
|
||||
'forms-textarea': {
|
||||
'border-color': 'var(--c--components--forms-textarea--border-color)',
|
||||
'border-color-hover':
|
||||
'var(--c--components--forms-textarea--border-color)',
|
||||
'border-radius': {
|
||||
hover: 'var(--c--components--forms-textarea--border-radius)',
|
||||
focus: 'var(--c--components--forms-textarea--border-radius)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
disabled: {
|
||||
'border-color-hover': 'var(--c--theme--colors--greyscale-200)',
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
'background-color': '#fff',
|
||||
},
|
||||
button: {
|
||||
'border-radius': {
|
||||
active: 'var(--c--components--button--border-radius)',
|
||||
},
|
||||
'medium-height': 'auto',
|
||||
'small-height': 'auto',
|
||||
success: {
|
||||
color: 'white',
|
||||
'color-disabled': 'white',
|
||||
'color-hover': 'white',
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--success-600)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-300)',
|
||||
'color-hover': 'var(--c--theme--colors--success-800)',
|
||||
},
|
||||
},
|
||||
danger: {
|
||||
'color-hover': 'white',
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--danger-600)',
|
||||
'color-hover': '#FF2725',
|
||||
'color-disabled': 'var(--c--theme--colors--danger-100)',
|
||||
},
|
||||
},
|
||||
primary: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'color-active': 'var(--c--theme--colors--primary-text)',
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--primary-400)',
|
||||
'color-active': 'var(--c--theme--colors--primary-500)',
|
||||
},
|
||||
border: {
|
||||
'color-active': 'transparent',
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
background: {
|
||||
color: 'white',
|
||||
'color-hover': 'var(--c--theme--colors--primary-700)',
|
||||
},
|
||||
border: {
|
||||
color: 'var(--c--theme--colors--greyscale-300)',
|
||||
},
|
||||
},
|
||||
tertiary: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--primary-100)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-300)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
color: 'white',
|
||||
background: {
|
||||
color: '#b3cef0',
|
||||
},
|
||||
},
|
||||
},
|
||||
'la-gauffre': {
|
||||
activated: false,
|
||||
},
|
||||
'home-proconnect': {
|
||||
activated: false,
|
||||
},
|
||||
},
|
||||
import { cunninghamConfig as tokens } from '@gouvfr-lasuite/ui-kit';
|
||||
|
||||
const customColors = {
|
||||
'primary-action': '#1212FF',
|
||||
'primary-bg': '#FAFAFA',
|
||||
'blue-400': '#7AB1E8',
|
||||
'blue-500': '#417DC4',
|
||||
'blue-600': '#3558A2',
|
||||
'brown-400': '#E6BE92',
|
||||
'brown-500': '#BD987A',
|
||||
'brown-600': '#745B47',
|
||||
'cyan-400': '#34BAB5',
|
||||
'cyan-500': '#009099',
|
||||
'cyan-600': '#006A6F',
|
||||
'gold-400': '#FFCA00',
|
||||
'gold-500': '#C3992A',
|
||||
'gold-600': '#695240',
|
||||
'green-400': '#34CB6A',
|
||||
'green-500': '#00A95F',
|
||||
'green-600': '#297254',
|
||||
'olive-400': '#99C221',
|
||||
'olive-500': '#68A532',
|
||||
'olive-600': '#447049',
|
||||
'orange-400': '#FF732C',
|
||||
'orange-500': '#E4794A',
|
||||
'orange-600': '#755348',
|
||||
'pink-400': '#FFB7AE',
|
||||
'pink-500': '#E18B76',
|
||||
'pink-600': '#8D533E',
|
||||
'purple-400': '#CE70CC',
|
||||
'purple-500': '#A558A0',
|
||||
'purple-600': '#6E445A',
|
||||
'yellow-400': '#D8C634',
|
||||
'yellow-500': '#B7A73F',
|
||||
'yellow-600': '#66673D',
|
||||
};
|
||||
|
||||
tokens.themes.default.theme.colors = {
|
||||
...tokens.themes.default.theme.colors,
|
||||
...customColors,
|
||||
};
|
||||
|
||||
tokens.themes.default.components = {
|
||||
...tokens.themes.default.components,
|
||||
...{
|
||||
'la-gauffre': {
|
||||
activated: true,
|
||||
},
|
||||
dsfr: {
|
||||
theme: {
|
||||
colors: {
|
||||
'card-border': '#E5E5E5',
|
||||
'primary-text': '#000091',
|
||||
'primary-100': '#ECECFE',
|
||||
'primary-150': '#F4F4FD',
|
||||
'primary-200': '#E3E3FD',
|
||||
'primary-300': '#CACAFB',
|
||||
'primary-400': '#8585F6',
|
||||
'primary-500': '#6A6AF4',
|
||||
'primary-600': '#313178',
|
||||
'primary-700': '#272747',
|
||||
'primary-800': '#000091',
|
||||
'primary-900': '#21213F',
|
||||
'secondary-text': '#fff',
|
||||
'secondary-100': '#fee9ea',
|
||||
'secondary-200': '#fedfdf',
|
||||
'secondary-300': '#fdbfbf',
|
||||
'secondary-400': '#e1020f',
|
||||
'secondary-500': '#c91a1f',
|
||||
'secondary-600': '#5e2b2b',
|
||||
'secondary-700': '#3b2424',
|
||||
'secondary-800': '#341f1f',
|
||||
'secondary-900': '#2b1919',
|
||||
'greyscale-text': '#303C4B',
|
||||
'greyscale-000': '#fff',
|
||||
'greyscale-050': '#F6F6F6',
|
||||
'greyscale-100': '#eee',
|
||||
'greyscale-200': '#E5E5E5',
|
||||
'greyscale-250': '#ddd',
|
||||
'greyscale-300': '#CECECE',
|
||||
'greyscale-350': '#ddd',
|
||||
'greyscale-400': '#929292',
|
||||
'greyscale-500': '#7C7C7C',
|
||||
'greyscale-600': '#666666',
|
||||
'greyscale-700': '#3A3A3A',
|
||||
'greyscale-750': '#353535',
|
||||
'greyscale-800': '#2A2A2A',
|
||||
'greyscale-900': '#242424',
|
||||
'greyscale-950': '#1E1E1E',
|
||||
'greyscale-1000': '#161616',
|
||||
'success-text': '#1f8d49',
|
||||
'success-100': '#dffee6',
|
||||
'success-200': '#b8fec9',
|
||||
'success-300': '#88fdaa',
|
||||
'success-400': '#3bea7e',
|
||||
'success-500': '#1f8d49',
|
||||
'success-600': '#18753c',
|
||||
'success-700': '#204129',
|
||||
'success-800': '#1e2e22',
|
||||
'success-900': '#19281d',
|
||||
'info-text': '#0078f3',
|
||||
'info-100': '#E8EDFF',
|
||||
'info-200': '#DDE5FF',
|
||||
'info-300': '#BCCDFF',
|
||||
'info-400': '#518FFF',
|
||||
'info-500': '#0078F3',
|
||||
'info-600': '#0063CB',
|
||||
'info-700': '#273961',
|
||||
'info-800': '#222A3F',
|
||||
'info-900': '#1D2437',
|
||||
'warning-text': '#d64d00',
|
||||
'warning-100': '#fff4f3',
|
||||
'warning-200': '#ffe9e6',
|
||||
'warning-300': '#ffded9',
|
||||
'warning-400': '#ffbeb4',
|
||||
'warning-500': '#d64d00',
|
||||
'warning-600': '#b34000',
|
||||
'warning-700': '#5e2c21',
|
||||
'warning-800': '#3e241e',
|
||||
'warning-900': '#361e19',
|
||||
'danger-text': '#FFF',
|
||||
'danger-100': '#FFE9E9',
|
||||
'danger-200': '#FFDDDD',
|
||||
'danger-300': '#FFBDBD',
|
||||
'danger-400': '#FF5655',
|
||||
'danger-500': '#F60700',
|
||||
'danger-600': '#CE0500',
|
||||
'danger-700': '#642626',
|
||||
'danger-800': '#412121',
|
||||
'danger-900': '#391C1C',
|
||||
},
|
||||
font: {
|
||||
families: {
|
||||
accent: 'Marianne',
|
||||
base: 'Marianne',
|
||||
},
|
||||
},
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
alert: {
|
||||
'border-radius': '0',
|
||||
error: {
|
||||
'background-color': 'var(--c--theme--colors--danger-100)',
|
||||
'border-left-color': 'var(--c--theme--colors--danger-400)',
|
||||
close: {
|
||||
color: 'white',
|
||||
'background-color': 'var(--c--theme--colors--danger-400)',
|
||||
'background-color-hover': 'var(--c--theme--colors--danger-600)',
|
||||
},
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
'width-small': '342px',
|
||||
},
|
||||
button: {
|
||||
'medium-height': '40px',
|
||||
'medium-text-height': '40px',
|
||||
'border-radius': '4px',
|
||||
primary: {
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'color-hover': '#1212ff',
|
||||
'color-active': '#2323ff',
|
||||
},
|
||||
color: '#fff',
|
||||
'color-hover': '#fff',
|
||||
'color-active': '#fff',
|
||||
},
|
||||
'primary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
color: 'var(--c--theme--colors--primary-800)',
|
||||
},
|
||||
secondary: {
|
||||
background: {
|
||||
'color-hover': '#F6F6F6',
|
||||
'color-active': '#EDEDED',
|
||||
},
|
||||
border: {
|
||||
color: 'var(--c--theme--colors--greyscale-300)',
|
||||
'color-hover': 'var(--c--theme--colors--greyscale-300)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
'tertiary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--greyscale-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
},
|
||||
datagrid: {
|
||||
header: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
size: 'var(--c--theme--font--sizes--s)',
|
||||
},
|
||||
body: {
|
||||
'background-color': 'transparent',
|
||||
'background-color-hover': '#F4F4FD',
|
||||
},
|
||||
pagination: {
|
||||
'background-color': 'transparent',
|
||||
'background-color-active': 'var(--c--theme--colors--primary-300)',
|
||||
'border-color': 'var(--c--theme--colors--primary-400)',
|
||||
},
|
||||
},
|
||||
'forms-checkbox': {
|
||||
'border-radius': '0',
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
text: {
|
||||
color: 'var(--c--theme--colors--greyscale-text)',
|
||||
size: 'var(--c--theme--font--sizes--t)',
|
||||
},
|
||||
},
|
||||
'forms-datepicker': {
|
||||
'border-radius': '0',
|
||||
},
|
||||
'forms-fileuploader': {
|
||||
'border-radius': '0',
|
||||
},
|
||||
'forms-field': {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'footer-font-size': 'var(--c--theme--font--sizes--t)',
|
||||
'footer-color': 'var(--c--theme--colors--greyscale-text)',
|
||||
},
|
||||
'forms-input': {
|
||||
'border-radius': '4px',
|
||||
'background-color': '#fff',
|
||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||
'value-color': 'var(--c--theme--colors--primary-text)',
|
||||
'font-size': '14px',
|
||||
},
|
||||
'forms-labelledbox': {
|
||||
'label-color': {
|
||||
big: 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
},
|
||||
'forms-radio': {
|
||||
'accent-color': 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
'forms-select': {
|
||||
'item-font-size': '14px',
|
||||
'border-radius': '4px',
|
||||
'border-radius-hover': '4px',
|
||||
'background-color': '#fff',
|
||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||
'border-color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
'forms-switch': {
|
||||
'handle-border-radius': '2px',
|
||||
'rail-border-radius': '4px',
|
||||
'accent-color': 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
'forms-textarea': {
|
||||
'border-radius': '0',
|
||||
},
|
||||
'la-gauffre': {
|
||||
activated: true,
|
||||
},
|
||||
'home-proconnect': {
|
||||
activated: true,
|
||||
},
|
||||
},
|
||||
'home-proconnect': {
|
||||
activated: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
export default tokens;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "2.6.0",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -21,7 +21,9 @@
|
||||
"@blocknote/react": "0.23.2-hotfix.0",
|
||||
"@blocknote/xl-docx-exporter": "0.23.2-hotfix.0",
|
||||
"@blocknote/xl-pdf-exporter": "0.23.2-hotfix.0",
|
||||
"@fontsource/material-icons": "5.2.5",
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@gouvfr-lasuite/ui-kit": "0.1.3",
|
||||
"@hocuspocus/provider": "2.15.2",
|
||||
"@openfun/cunningham-react": "3.0.0",
|
||||
"@react-pdf/renderer": "4.1.6",
|
||||
@@ -36,7 +38,7 @@
|
||||
"idb": "8.0.2",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.5.0",
|
||||
"next": "15.2.1",
|
||||
"next": "15.2.3",
|
||||
"posthog-js": "1.227.0",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.6.0",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,101 +0,0 @@
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Thin.woff2') format('woff2'),
|
||||
url('Marianne-Thin.woff') format('woff');
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Thin_Italic.woff2') format('woff2'),
|
||||
url('Marianne-Thin_Italic.woff') format('woff');
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Light.woff2') format('woff2'),
|
||||
url('Marianne-Light.woff') format('woff');
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Light_Italic.woff2') format('woff2'),
|
||||
url('Marianne-Light_Italic.woff') format('woff');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Regular.woff2') format('woff2'),
|
||||
url('Marianne-Regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Regular_Italic.woff2') format('woff2'),
|
||||
url('Marianne-Regular_Italic.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Medium.woff2') format('woff2'),
|
||||
url('Marianne-Medium.woff') format('woff');
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Medium_Italic.woff2') format('woff2'),
|
||||
url('Marianne-Medium_Italic.woff') format('woff');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Bold.woff2') format('woff2'),
|
||||
url('Marianne-Bold.woff') format('woff');
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Bold_Italic.woff2') format('woff2'),
|
||||
url('Marianne-Bold_Italic.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-ExtraBold.woff2') format('woff2'),
|
||||
url('Marianne-ExtraBold.woff') format('woff');
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-ExtraBold_Italic.woff2') format('woff2'),
|
||||
url('Marianne-ExtraBold_Italic.woff') format('woff');
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCunninghamTheme } from '../useCunninghamTheme';
|
||||
|
||||
describe('<useCunninghamTheme />', () => {
|
||||
it('has the dsfr logo correctly set', () => {
|
||||
it('has the logo correctly set', () => {
|
||||
const { themeTokens, setTheme } = useCunninghamTheme.getState();
|
||||
setTheme('dsfr');
|
||||
setTheme('default');
|
||||
const logo = themeTokens().logo;
|
||||
expect(logo?.src).toBe('/assets/logo-gouv.svg');
|
||||
expect(logo?.widthHeader).toBe('110px');
|
||||
|
||||
@@ -25,4 +25,11 @@
|
||||
--c--components--forms-select--value-color--disabled: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
|
||||
/**
|
||||
* Button
|
||||
**/
|
||||
--c--components--button--border-radius--active: var(
|
||||
--c--components--button--border-radius
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,633 +0,0 @@
|
||||
@import url('@openfun/cunningham-react/icons');
|
||||
@import url('@openfun/cunningham-react/style');
|
||||
@import url('@openfun/cunningham-react/fonts');
|
||||
@import url('./cunningham-tokens.css');
|
||||
@import url('./cunningham-custom-tokens.css');
|
||||
@import url('../assets/fonts/Marianne/Marianne-font.css');
|
||||
|
||||
.c__input,
|
||||
.c__field,
|
||||
.c__select,
|
||||
.c__datagrid {
|
||||
font-family: var(--c--theme--font--families--base);
|
||||
}
|
||||
|
||||
.c__field {
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.c__field .c__field__footer {
|
||||
padding: 2px 0 0;
|
||||
font-size: var(--c--components--forms-field--footer-font-size);
|
||||
color: var(--c--components--forms-field--footer-color);
|
||||
}
|
||||
|
||||
.labelled-box label {
|
||||
color: var(--c--theme--colors--primary-text);
|
||||
}
|
||||
|
||||
.labelled-box--disabled label {
|
||||
color: var(--c--components--forms-labelledbox--label-color--small-disabled);
|
||||
}
|
||||
|
||||
.c__field :not(.c__textarea__wrapper, div) .labelled-box label.placeholder {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/**
|
||||
* Input
|
||||
* TextArea
|
||||
*/
|
||||
.c__input__wrapper,
|
||||
.c__textarea__wrapper {
|
||||
transition: all var(--c--theme--transitions--duration)
|
||||
var(--c--theme--transitions--ease-out);
|
||||
}
|
||||
|
||||
.c__input__wrapper:has(input[readonly]),
|
||||
.c__input__wrapper:has(input[readonly]) * {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.c__textarea__wrapper:has(input.border-none),
|
||||
.c__textarea__wrapper:has(input.border-none) *,
|
||||
.c__input__wrapper:has(input.border-none),
|
||||
.c__input__wrapper:has(input.border-none) * {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.c__input__wrapper:hover,
|
||||
.c__textarea__wrapper:hover {
|
||||
box-shadow: var(--c--components--forms-input--box-shadow-color) 0 0 0 2px;
|
||||
}
|
||||
|
||||
.c__textarea__wrapper--disabled:hover,
|
||||
.c__input__wrapper--disabled:hover,
|
||||
.c__input__wrapper:hover:has(input[readonly]) {
|
||||
box-shadow: var(--c--theme--colors--primary-500) 0 0 0 0;
|
||||
}
|
||||
|
||||
.c__input__wrapper--disabled {
|
||||
color: var(--c--components--forms-input--value-color--disabled);
|
||||
}
|
||||
|
||||
.c__input__wrapper .labelled-box__label.placeholder {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.c__input__wrapper .c__input,
|
||||
.c__textarea__wrapper .c__textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c__input__wrapper--disabled .c__input {
|
||||
color: var(--c--components--forms-input--value-color--disabled);
|
||||
}
|
||||
|
||||
.c__input__wrapper--error .c__input {
|
||||
color: var(--c--components--forms-input--color--error);
|
||||
}
|
||||
|
||||
.c__input__wrapper--error:not(.c__input__wrapper--disabled):hover {
|
||||
border-color: var(--c--components--forms-input--border--color-error-hover);
|
||||
color: var(--c--components--forms-input--color--error-hover);
|
||||
}
|
||||
|
||||
.c__input__wrapper--error:hover {
|
||||
box-shadow: var(--c--components--forms-input--color--box-shadow-error-hover) 0
|
||||
0 0 2px;
|
||||
}
|
||||
|
||||
.c__input__wrapper--error:not(.c__input__wrapper--disabled):hover label {
|
||||
color: var(--c--components--forms-input--border--color-error-hover);
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:focus {
|
||||
transition:
|
||||
background-color 0s 600000s,
|
||||
color 0s 600000s;
|
||||
}
|
||||
|
||||
.c__textarea__wrapper .c__textarea {
|
||||
color: var(--c--components--forms-textarea--color);
|
||||
}
|
||||
|
||||
.c__textarea__wrapper:hover {
|
||||
border-color: var(--c--components--forms-textarea--border-color-hover);
|
||||
}
|
||||
|
||||
.c__textarea__wrapper--disabled:hover {
|
||||
border-color: var(
|
||||
--c--components--forms-textarea--disabled--border-color-hover
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select
|
||||
*/
|
||||
.c_select__no_border .c__select .c__select__wrapper,
|
||||
.c_select__no_border .c__select .c__select__wrapper:hover,
|
||||
.c_select__no_border
|
||||
.c__select:not(.c__select--disabled)
|
||||
.c__select__wrapper:hover {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.c__select__wrapper {
|
||||
transition: all var(--c--theme--transitions--duration)
|
||||
var(--c--theme--transitions--ease-out);
|
||||
min-height: var(--c--components--forms-select--height);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.c__select:not(.c__select--disabled) .c__select__wrapper:hover {
|
||||
box-shadow: var(--c--components--forms-input--box-shadow-color) 0 0 0 2px;
|
||||
}
|
||||
|
||||
.c__select__wrapper:hover {
|
||||
border-radius: var(--c--components--forms-select--border-radius-hover);
|
||||
border-color: var(--c--components--forms-select--border-color-hover);
|
||||
}
|
||||
|
||||
.c__select--disabled .c__select__wrapper:hover {
|
||||
border-color: var(--c--components--forms-select--border-color-disabled-hover);
|
||||
}
|
||||
|
||||
.c__select--disabled .c__select__wrapper label,
|
||||
.c__select--disabled .c__select__wrapper input,
|
||||
.c__select--disabled .c__select__wrapper {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.c__select__menu__item {
|
||||
transition: all var(--c--theme--transitions--duration)
|
||||
var(--c--theme--transitions--ease-out);
|
||||
}
|
||||
|
||||
.c__select--disabled .c__select__wrapper label,
|
||||
.c__select--disabled .c__select__wrapper input,
|
||||
.c_select__no_bg .c__select__wrapper {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.c__select__wrapper:focus-within .labelled-box--disabled label {
|
||||
color: var(--c--components--forms-labelledbox--label-color--small-disabled);
|
||||
}
|
||||
|
||||
.c__select__wrapper .labelled-box {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.c__select__wrapper .labelled-box .labelled-box__children {
|
||||
padding: unset;
|
||||
padding-right: 5rem;
|
||||
}
|
||||
|
||||
.c__select__wrapper .labelled-box .c__select__inner__actions {
|
||||
right: 0;
|
||||
top: 50%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.c__select__wrapper label {
|
||||
position: relative;
|
||||
padding-right: 5rem;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.c__select__wrapper .c__select__inner__actions__open:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.c__select__wrapper .labelled-box__label.c__offscreen {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* DataGrid
|
||||
*/
|
||||
.c__datagrid__table__container {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.c__datagrid__table__container > table th .c__datagrid__header {
|
||||
color: var(--c--components--datagrid--header--color);
|
||||
font-weight: var(--c--components--datagrid--header--weight);
|
||||
font-size: var(--c--components--datagrid--header--size);
|
||||
padding-block: 2rem;
|
||||
}
|
||||
|
||||
.c__datagrid__table__container > table tbody tr {
|
||||
border: none;
|
||||
border-top: 1px var(--c--theme--colors--greyscale-100) solid;
|
||||
border-bottom: 1px var(--c--theme--colors--greyscale-100) solid;
|
||||
}
|
||||
|
||||
.c__datagrid__table__container > table tbody {
|
||||
background-color: var(--c--components--datagrid--body--background-color);
|
||||
}
|
||||
|
||||
.c__datagrid__table__container > table tbody tr:hover {
|
||||
background-color: var(
|
||||
--c--components--datagrid--body--background-color-hover
|
||||
);
|
||||
}
|
||||
|
||||
.c__datagrid__table__container > table {
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
.c__datagrid__table__container > table td {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.c__datagrid__table__container > table th:first-child,
|
||||
.c__datagrid__table__container > table td:first-child {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.c__datagrid > .c__pagination {
|
||||
padding-inline: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.c__pagination__list {
|
||||
gap: 3px;
|
||||
border-radius: 4px;
|
||||
background: var(--c--components--datagrid--pagination--background-color);
|
||||
border-color: var(--c--components--datagrid--pagination--border-color);
|
||||
}
|
||||
|
||||
.c__pagination__list .c__button--tertiary-text.c__button--active {
|
||||
background-color: var(
|
||||
--c--components--datagrid--pagination--background-color-active
|
||||
);
|
||||
color: var(--c--theme--colors--greyscale-800);
|
||||
}
|
||||
|
||||
.c__pagination__list .c__button--tertiary-text:disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (width <= 380px) {
|
||||
.c__datagrid > .c__pagination {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Date picker
|
||||
*/
|
||||
.c__popover.c__popover--borderless {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.c__date-picker__wrapper {
|
||||
transition: all var(--c--theme--transitions--duration)
|
||||
var(--c--theme--transitions--ease-out);
|
||||
}
|
||||
|
||||
.c__date-picker:not(.c__date-picker--disabled):hover .c__date-picker__wrapper {
|
||||
box-shadow: var(--c--theme--colors--primary-500) 0 0 0 2px;
|
||||
}
|
||||
|
||||
.c__date-picker.c__date-picker--invalid:not(.c__date-picker--disabled):hover
|
||||
.c__date-picker__wrapper {
|
||||
box-shadow: var(--c--theme--colors--danger-300) 0 0 0 2px;
|
||||
}
|
||||
|
||||
.c__date-picker__wrapper button[aria-label='Clear date'],
|
||||
.c__date-picker.c__date-picker--invalid .c__date-picker__wrapper * {
|
||||
color: var(--c--theme--colors--danger-300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox
|
||||
*/
|
||||
.c__checkbox:focus-within {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.c__checkbox {
|
||||
transition: all 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
.c__checkbox .c__field__text {
|
||||
color: var(--c--components--forms-checkbox--text--color);
|
||||
font-size: var(--c--components--forms-checkbox--text--size);
|
||||
}
|
||||
|
||||
.c__checkbox.c__checkbox--disabled .c__field__text {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
}
|
||||
|
||||
.c__switch.c__checkbox--disabled .c__switch__rail {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.c__checkbox.c__checkbox--disabled .c__checkbox__label {
|
||||
color: var(--c--theme--colors--greyscale-400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Button
|
||||
*/
|
||||
.c__button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c__button:hover.c__button-no-bg,
|
||||
.c__button.c__button-no-bg,
|
||||
.c__button:disabled.c__button-no-bg {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.c__button--nano {
|
||||
padding: 0 var(--c--theme--spacings--3xs);
|
||||
gap: var(--c--theme--spacings--3xs);
|
||||
}
|
||||
|
||||
.c__button--nano.c__button--icon-only {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.c__button--nano.c__button--icon-only.c__button--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c__button--medium {
|
||||
height: auto;
|
||||
min-height: var(--c--components--button--medium-height);
|
||||
}
|
||||
|
||||
.c__button--small {
|
||||
padding: 0.6rem 0.75rem;
|
||||
}
|
||||
|
||||
.c__button--with-icon--right {
|
||||
padding: 0.7rem var(--c--theme--spacings--t) 0.7rem
|
||||
var(--c--theme--spacings--s);
|
||||
}
|
||||
|
||||
.c__button--primary {
|
||||
background-color: var(--c--components--button--primary--background--color);
|
||||
color: var(--c--components--button--primary--color);
|
||||
}
|
||||
|
||||
.c__button--primary:hover {
|
||||
background-color: var(
|
||||
--c--components--button--primary--background--color-hover
|
||||
);
|
||||
color: var(--c--components--button--primary--color-hover);
|
||||
}
|
||||
|
||||
.c__button--primary:active,
|
||||
.c__button--primary.c__button--active {
|
||||
background-color: var(
|
||||
--c--components--button--primary--background--color-active
|
||||
);
|
||||
color: var(--c--components--button--primary--color-active);
|
||||
border-color: var(--c--components--button--primary--border--color-active);
|
||||
}
|
||||
|
||||
.c__button--primary-text:active,
|
||||
.c__button--primary-text.c__button--active {
|
||||
border: none;
|
||||
background-color: var(
|
||||
--c--components--button--primary-text--background--color-active
|
||||
);
|
||||
}
|
||||
|
||||
.c__button--primary-text {
|
||||
color: var(--c--components--button--primary-text--color);
|
||||
}
|
||||
|
||||
.c__button--primary-text:hover,
|
||||
.c__button--primary-text:focus-visible {
|
||||
background-color: var(
|
||||
--c--components--button--primary-text--background--color-hover
|
||||
);
|
||||
color: var(--c--components--button--primary-text--color-hover);
|
||||
}
|
||||
|
||||
.c__button:disabled {
|
||||
background-color: var(--c--components--button--disabled--background--color);
|
||||
color: var(--c--components--button--disabled--color);
|
||||
}
|
||||
|
||||
.c__button--success {
|
||||
background-color: var(--c--components--button--success--background--color);
|
||||
color: var(--c--components--button--success--color);
|
||||
}
|
||||
|
||||
.c__button--success:hover,
|
||||
.c__button--success:focus-visible {
|
||||
background-color: var(
|
||||
--c--components--button--success--background--color-hover
|
||||
);
|
||||
color: var(--c--components--button--success--color-hover);
|
||||
}
|
||||
|
||||
.c__button--success:disabled {
|
||||
background-color: var(
|
||||
--c--components--button--success--background--color-disabled
|
||||
);
|
||||
color: var(--c--components--button--success--color-disabled);
|
||||
}
|
||||
|
||||
.c__button--secondary {
|
||||
background-color: var(--c--components--button--secondary--background--color);
|
||||
color: var(--c--components--button--secondary--color);
|
||||
border: 1px solid var(--c--components--button--secondary--border--color);
|
||||
}
|
||||
|
||||
.c__button--secondary:hover,
|
||||
.c__button--secondary:focus-visible {
|
||||
background-color: var(
|
||||
--c--components--button--secondary--background--color-hover
|
||||
);
|
||||
color: var(--c--components--button--secondary--color-hover);
|
||||
border: 1px solid var(--c--components--button--secondary--border--color-hover);
|
||||
}
|
||||
|
||||
.c__button--tertiary {
|
||||
background-color: var(--c--components--button--tertiary--background--color);
|
||||
color: var(--c--components--button--tertiary--color);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.c__button--tertiary:hover,
|
||||
.c__button--tertiary:focus-visible {
|
||||
background-color: var(
|
||||
--c--components--button--tertiary--background--color-hover
|
||||
);
|
||||
color: var(--c--components--button--tertiary--color);
|
||||
}
|
||||
|
||||
.c__button--tertiary:active {
|
||||
background-color: var(
|
||||
--c--components--button--tertiary--background--color-active
|
||||
);
|
||||
color: var(--c--components--button--tertiary--color-active);
|
||||
}
|
||||
|
||||
.c__button--tertiary:disabled {
|
||||
background-color: var(
|
||||
--c--components--button--tertiary--background--color-disabled
|
||||
);
|
||||
color: var(--c--components--button--tertiary--color-disabled);
|
||||
}
|
||||
|
||||
.c__button--tertiary-text {
|
||||
border: none;
|
||||
color: var(--c--components--button--tertiary-text--color);
|
||||
}
|
||||
|
||||
.c__button--tertiary-text:hover,
|
||||
.c__button--tertiary-text:focus-visible {
|
||||
background-color: var(
|
||||
--c--components--button--tertiary-text--background--color-hover
|
||||
);
|
||||
color: var(--c--components--button--tertiary-text--color-hover);
|
||||
}
|
||||
|
||||
.c__button--tertiary-text:disabled {
|
||||
background-color: var(
|
||||
--c--components--button--tertiary-text--background--color-disabled
|
||||
);
|
||||
color: var(--c--components--button--tertiary-text--color-disabled);
|
||||
}
|
||||
|
||||
.c__button--danger {
|
||||
background-color: var(--c--components--button--danger--background--color);
|
||||
}
|
||||
|
||||
.c__button--danger:hover,
|
||||
.c__button--danger:focus-visible {
|
||||
background-color: var(
|
||||
--c--components--button--danger--background--color-hover
|
||||
);
|
||||
color: var(--c--components--button--danger--color-hover);
|
||||
}
|
||||
|
||||
.c__button--danger:disabled {
|
||||
background-color: var(
|
||||
--c--components--button--danger--background--color-disabled
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal
|
||||
*/
|
||||
.c__modal__backdrop {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.c__modal__close .c__button--tertiary-text:hover,
|
||||
.c__modal__close .c__button--tertiary-text:focus-visible {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.c__modal__close button {
|
||||
padding: 0;
|
||||
font-size: 88px;
|
||||
width: 28px !important;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.c__modal__close button .material-icons {
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
}
|
||||
|
||||
.c__modal__close .c__button {
|
||||
padding: 0 !important;
|
||||
top: -0.65rem;
|
||||
right: -0.65rem;
|
||||
}
|
||||
|
||||
.c__modal--full .c__modal__content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.c__modal__title {
|
||||
padding: 0;
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: var(--c--theme--spacings--2xs);
|
||||
}
|
||||
|
||||
@media screen and (width <= 420px) {
|
||||
.c__modal__scroller {
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.c__modal__title h2 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 576px) {
|
||||
.c__modal__footer--sided {
|
||||
gap: 0.5rem;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.c__modal__scroller:has(.noPadding) {
|
||||
padding: 0 !important;
|
||||
|
||||
.c__modal__close .c__button {
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.c__modal__title {
|
||||
font-size: var(--c--theme--font--sizes--xs);
|
||||
padding: var(--c--theme--spacings--base);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast
|
||||
*/
|
||||
.c__toast__container {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tooltip
|
||||
*/
|
||||
.c__tooltip {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert
|
||||
*/
|
||||
.c__alert--error {
|
||||
background-color: var(--c--components--alert--error--background-color);
|
||||
border-left-color: var(--c--components--alert--error--border-left-color);
|
||||
}
|
||||
|
||||
.c__alert--error .c__button--tertiary {
|
||||
background-color: var(--c--components--alert--error--close--background-color);
|
||||
color: var(--c--components--alert--error--close--color);
|
||||
}
|
||||
|
||||
.c__alert.c__alert--error .c__button--tertiary:hover {
|
||||
background-color: var(
|
||||
--c--components--alert--error--close--background-color-hover
|
||||
);
|
||||
}
|
||||
@@ -1,82 +1,85 @@
|
||||
:root {
|
||||
--c--theme--colors--secondary-text: var(--c--theme--colors--greyscale-700);
|
||||
--c--theme--colors--secondary-100: #f2f7fc;
|
||||
--c--theme--colors--secondary-200: #ebf3fa;
|
||||
--c--theme--colors--secondary-300: #e2eef8;
|
||||
--c--theme--colors--secondary-400: #ddeaf7;
|
||||
--c--theme--colors--secondary-500: #d4e5f5;
|
||||
--c--theme--colors--secondary-600: #c1d0df;
|
||||
--c--theme--colors--secondary-700: #97a3ae;
|
||||
--c--theme--colors--secondary-800: #757e87;
|
||||
--c--theme--colors--secondary-900: #596067;
|
||||
--c--theme--colors--info-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--colors--info-100: #ebf2fc;
|
||||
--c--theme--colors--info-200: #8cb5ea;
|
||||
--c--theme--colors--info-300: #5894e1;
|
||||
--c--theme--colors--info-400: #377fdb;
|
||||
--c--theme--colors--info-500: #055fd2;
|
||||
--c--theme--colors--info-600: #0556bf;
|
||||
--c--theme--colors--info-700: #044395;
|
||||
--c--theme--colors--info-800: #033474;
|
||||
--c--theme--colors--info-900: #022858;
|
||||
--c--theme--colors--greyscale-100: #fafafb;
|
||||
--c--theme--colors--greyscale-200: #f3f4f4;
|
||||
--c--theme--colors--greyscale-300: #e7e8ea;
|
||||
--c--theme--colors--greyscale-400: #c2c6ca;
|
||||
--c--theme--colors--greyscale-500: #9ea3aa;
|
||||
--c--theme--colors--greyscale-600: #79818a;
|
||||
--c--theme--colors--greyscale-700: #555f6b;
|
||||
--c--theme--colors--greyscale-800: #303c4b;
|
||||
--c--theme--colors--greyscale-900: #0c1a2b;
|
||||
--c--theme--colors--secondary-text: #fff;
|
||||
--c--theme--colors--secondary-100: #fee9ea;
|
||||
--c--theme--colors--secondary-200: #fedfdf;
|
||||
--c--theme--colors--secondary-300: #fdbfbf;
|
||||
--c--theme--colors--secondary-400: #e1020f;
|
||||
--c--theme--colors--secondary-500: #c91a1f;
|
||||
--c--theme--colors--secondary-600: #5e2b2b;
|
||||
--c--theme--colors--secondary-700: #3b2424;
|
||||
--c--theme--colors--secondary-800: #341f1f;
|
||||
--c--theme--colors--secondary-900: #2b1919;
|
||||
--c--theme--colors--info-text: #0078f3;
|
||||
--c--theme--colors--info-100: #e8edff;
|
||||
--c--theme--colors--info-200: #dde5ff;
|
||||
--c--theme--colors--info-300: #bccdff;
|
||||
--c--theme--colors--info-400: #518fff;
|
||||
--c--theme--colors--info-500: #0078f3;
|
||||
--c--theme--colors--info-600: #0063cb;
|
||||
--c--theme--colors--info-700: #273961;
|
||||
--c--theme--colors--info-800: #222a3f;
|
||||
--c--theme--colors--info-900: #1d2437;
|
||||
--c--theme--colors--greyscale-100: #eee;
|
||||
--c--theme--colors--greyscale-200: #e5e5e5;
|
||||
--c--theme--colors--greyscale-300: #cecece;
|
||||
--c--theme--colors--greyscale-400: #929292;
|
||||
--c--theme--colors--greyscale-500: #7c7c7c;
|
||||
--c--theme--colors--greyscale-600: #666;
|
||||
--c--theme--colors--greyscale-700: #3a3a3a;
|
||||
--c--theme--colors--greyscale-800: #2a2a2a;
|
||||
--c--theme--colors--greyscale-900: #242424;
|
||||
--c--theme--colors--greyscale-000: #fff;
|
||||
--c--theme--colors--primary-100: #edf5fa;
|
||||
--c--theme--colors--primary-200: #8cb5ea;
|
||||
--c--theme--colors--primary-300: #5894e1;
|
||||
--c--theme--colors--primary-400: #377fdb;
|
||||
--c--theme--colors--primary-500: #055fd2;
|
||||
--c--theme--colors--primary-600: #0556bf;
|
||||
--c--theme--colors--primary-700: #044395;
|
||||
--c--theme--colors--primary-800: #033474;
|
||||
--c--theme--colors--primary-900: #022858;
|
||||
--c--theme--colors--success-100: #effcd3;
|
||||
--c--theme--colors--success-200: #dbfaa9;
|
||||
--c--theme--colors--success-300: #bef27c;
|
||||
--c--theme--colors--success-400: #a0e659;
|
||||
--c--theme--colors--success-500: #76d628;
|
||||
--c--theme--colors--success-600: #5ab81d;
|
||||
--c--theme--colors--success-700: #419a14;
|
||||
--c--theme--colors--success-800: #2c7c0c;
|
||||
--c--theme--colors--success-900: #1d6607;
|
||||
--c--theme--colors--warning-100: #fff8cd;
|
||||
--c--theme--colors--warning-200: #ffef9b;
|
||||
--c--theme--colors--warning-300: #ffe469;
|
||||
--c--theme--colors--warning-400: #ffda43;
|
||||
--c--theme--colors--warning-500: #ffc805;
|
||||
--c--theme--colors--warning-600: #dba603;
|
||||
--c--theme--colors--warning-700: #b78702;
|
||||
--c--theme--colors--warning-800: #936901;
|
||||
--c--theme--colors--warning-900: #7a5400;
|
||||
--c--theme--colors--danger-100: #f4b0b0;
|
||||
--c--theme--colors--danger-200: #ee8a8a;
|
||||
--c--theme--colors--danger-300: #e65454;
|
||||
--c--theme--colors--danger-400: #e13333;
|
||||
--c--theme--colors--danger-500: #da0000;
|
||||
--c--theme--colors--danger-600: #c60000;
|
||||
--c--theme--colors--danger-700: #9b0000;
|
||||
--c--theme--colors--danger-800: #780000;
|
||||
--c--theme--colors--danger-900: #5c0000;
|
||||
--c--theme--colors--primary-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--colors--success-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--colors--warning-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--colors--danger-text: var(--c--theme--colors--greyscale-000);
|
||||
--c--theme--colors--card-border: #ededed;
|
||||
--c--theme--colors--primary-bg: #fafafa;
|
||||
--c--theme--colors--primary-action: #1212ff;
|
||||
--c--theme--colors--primary-100: #ececfe;
|
||||
--c--theme--colors--primary-200: #e3e3fd;
|
||||
--c--theme--colors--primary-300: #cacafb;
|
||||
--c--theme--colors--primary-400: #8585f6;
|
||||
--c--theme--colors--primary-500: #6a6af4;
|
||||
--c--theme--colors--primary-600: #313178;
|
||||
--c--theme--colors--primary-700: #272747;
|
||||
--c--theme--colors--primary-800: #000091;
|
||||
--c--theme--colors--primary-900: #21213f;
|
||||
--c--theme--colors--success-100: #dffee6;
|
||||
--c--theme--colors--success-200: #b8fec9;
|
||||
--c--theme--colors--success-300: #88fdaa;
|
||||
--c--theme--colors--success-400: #3bea7e;
|
||||
--c--theme--colors--success-500: #1f8d49;
|
||||
--c--theme--colors--success-600: #18753c;
|
||||
--c--theme--colors--success-700: #204129;
|
||||
--c--theme--colors--success-800: #1e2e22;
|
||||
--c--theme--colors--success-900: #19281d;
|
||||
--c--theme--colors--warning-100: #fff4f3;
|
||||
--c--theme--colors--warning-200: #ffe9e6;
|
||||
--c--theme--colors--warning-300: #ffded9;
|
||||
--c--theme--colors--warning-400: #ffbeb4;
|
||||
--c--theme--colors--warning-500: #d64d00;
|
||||
--c--theme--colors--warning-600: #b34000;
|
||||
--c--theme--colors--warning-700: #5e2c21;
|
||||
--c--theme--colors--warning-800: #3e241e;
|
||||
--c--theme--colors--warning-900: #361e19;
|
||||
--c--theme--colors--danger-100: #ffe9e9;
|
||||
--c--theme--colors--danger-200: #fdd;
|
||||
--c--theme--colors--danger-300: #ffbdbd;
|
||||
--c--theme--colors--danger-400: #ff5655;
|
||||
--c--theme--colors--danger-500: #f60700;
|
||||
--c--theme--colors--danger-600: #ce0500;
|
||||
--c--theme--colors--danger-700: #642626;
|
||||
--c--theme--colors--danger-800: #412121;
|
||||
--c--theme--colors--danger-900: #391c1c;
|
||||
--c--theme--colors--primary-text: #000091;
|
||||
--c--theme--colors--success-text: #1f8d49;
|
||||
--c--theme--colors--warning-text: #d64d00;
|
||||
--c--theme--colors--danger-text: #fff;
|
||||
--c--theme--colors--primary-050: #f5f5fe;
|
||||
--c--theme--colors--primary-150: #e5eefa;
|
||||
--c--theme--colors--primary-950: #1b1b35;
|
||||
--c--theme--colors--info-150: #e5eefa;
|
||||
--c--theme--colors--primary-150: #f4f4fd;
|
||||
--c--theme--colors--greyscale-text: #303c4b;
|
||||
--c--theme--colors--greyscale-050: #f6f6f6;
|
||||
--c--theme--colors--greyscale-250: #ddd;
|
||||
--c--theme--colors--greyscale-350: #ddd;
|
||||
--c--theme--colors--greyscale-750: #353535;
|
||||
--c--theme--colors--greyscale-950: #1e1e1e;
|
||||
--c--theme--colors--greyscale-1000: #161616;
|
||||
--c--theme--colors--primary-action: #1212ff;
|
||||
--c--theme--colors--primary-bg: #fafafa;
|
||||
--c--theme--colors--blue-400: #7ab1e8;
|
||||
--c--theme--colors--blue-500: #417dc4;
|
||||
--c--theme--colors--blue-600: #3558a2;
|
||||
@@ -135,8 +138,8 @@
|
||||
--c--theme--font--weights--bold: 600;
|
||||
--c--theme--font--weights--extrabold: 800;
|
||||
--c--theme--font--weights--black: 900;
|
||||
--c--theme--font--families--base: 'Roboto Flex Variable', sans-serif;
|
||||
--c--theme--font--families--accent: 'Roboto Flex Variable', sans-serif;
|
||||
--c--theme--font--families--base: marianne;
|
||||
--c--theme--font--families--accent: marianne;
|
||||
--c--theme--font--letterspacings--h1: normal;
|
||||
--c--theme--font--letterspacings--h2: normal;
|
||||
--c--theme--font--letterspacings--h3: normal;
|
||||
@@ -182,170 +185,271 @@
|
||||
--c--theme--breakpoints--xl: 1200px;
|
||||
--c--theme--breakpoints--xxl: 1400px;
|
||||
--c--theme--breakpoints--xxs: 320px;
|
||||
--c--theme--logo--src: ;
|
||||
--c--theme--logo--widthheader: ;
|
||||
--c--theme--logo--widthfooter: ;
|
||||
--c--theme--logo--alt: ;
|
||||
--c--components--datagrid--header--weight: var(
|
||||
--c--theme--font--weights--extrabold
|
||||
);
|
||||
--c--components--datagrid--header--size: var(--c--theme--font--sizes--ml);
|
||||
--c--components--datagrid--cell--color: var(--c--theme--colors--primary-500);
|
||||
--c--components--datagrid--cell--size: var(--c--theme--font--sizes--ml);
|
||||
--c--components--forms-checkbox--background-color--hover: #055fd214;
|
||||
--c--components--forms-checkbox--color: var(--c--theme--colors--primary-500);
|
||||
--c--components--forms-checkbox--font-size: var(--c--theme--font--sizes--ml);
|
||||
--c--components--forms-datepicker--border-color: var(
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
--c--components--forms-datepicker--value-color: var(
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
--c--components--forms-datepicker--border-radius--hover: var(
|
||||
--c--components--forms-datepicker--border-radius
|
||||
);
|
||||
--c--components--forms-datepicker--border-radius--focus: var(
|
||||
--c--components--forms-datepicker--border-radius
|
||||
);
|
||||
--c--components--forms-field--color: var(--c--theme--colors--primary-500);
|
||||
--c--components--forms-field--value-color: var(
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
--c--components--forms-field--width: auto;
|
||||
--c--components--forms-input--value-color: var(
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
--c--components--forms-input--border-color: var(
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
--c--components--forms-input--color--error: var(
|
||||
--c--theme--colors--danger-500
|
||||
);
|
||||
--c--components--forms-input--color--error-hover: var(
|
||||
--c--theme--colors--danger-500
|
||||
);
|
||||
--c--components--forms-input--color--box-shadow-error-hover: var(
|
||||
--c--theme--colors--danger-500
|
||||
);
|
||||
--c--components--forms-labelledbox--label-color--small: var(
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
--c--components--forms-labelledbox--label-color--small-disabled: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--forms-labelledbox--label-color--big--disabled: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--forms-select--border-color: var(
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
--c--components--forms-select--border-color-disabled-hover: var(
|
||||
--c--theme--colors--greyscale-200
|
||||
);
|
||||
--c--components--forms-select--border-radius--hover: var(
|
||||
--c--components--forms-select--border-radius
|
||||
);
|
||||
--c--components--forms-select--border-radius--focus: var(
|
||||
--c--components--forms-select--border-radius
|
||||
);
|
||||
--c--components--forms-select--font-size: var(--c--theme--font--sizes--ml);
|
||||
--c--components--forms-select--menu-background-color: #fff;
|
||||
--c--components--forms-select--item-background-color--hover: var(
|
||||
--c--theme--colors--primary-300
|
||||
);
|
||||
--c--components--forms-switch--accent-color: var(
|
||||
--c--theme--colors--primary-400
|
||||
);
|
||||
--c--components--forms-textarea--border-color: var(
|
||||
--c--components--forms-textarea--border-color
|
||||
);
|
||||
--c--components--forms-textarea--border-color-hover: var(
|
||||
--c--components--forms-textarea--border-color
|
||||
);
|
||||
--c--components--forms-textarea--border-radius--hover: var(
|
||||
--c--components--forms-textarea--border-radius
|
||||
);
|
||||
--c--components--forms-textarea--border-radius--focus: var(
|
||||
--c--components--forms-textarea--border-radius
|
||||
);
|
||||
--c--components--forms-textarea--color: var(--c--theme--colors--primary-500);
|
||||
--c--components--forms-textarea--disabled--border-color-hover: var(
|
||||
--c--theme--colors--greyscale-200
|
||||
);
|
||||
--c--components--modal--background-color: #fff;
|
||||
--c--components--button--border-radius--active: var(
|
||||
--c--components--button--border-radius
|
||||
);
|
||||
--c--components--button--medium-height: auto;
|
||||
--c--components--button--small-height: auto;
|
||||
--c--components--button--success--color: white;
|
||||
--c--components--button--success--color-disabled: white;
|
||||
--c--components--button--success--color-hover: white;
|
||||
--c--components--button--success--background--color: var(
|
||||
--c--theme--colors--success-600
|
||||
);
|
||||
--c--components--button--success--background--color-disabled: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--button--success--background--color-hover: var(
|
||||
--c--theme--colors--success-800
|
||||
);
|
||||
--c--components--button--danger--color-hover: white;
|
||||
--c--components--button--danger--background--color: var(
|
||||
--c--theme--colors--danger-600
|
||||
);
|
||||
--c--components--button--danger--background--color-hover: #ff2725;
|
||||
--c--components--button--danger--background--color-disabled: var(
|
||||
--c--theme--colors--danger-100
|
||||
);
|
||||
--c--components--button--primary--color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--button--primary--color-active: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--theme--breakpoints--mobile: 768px;
|
||||
--c--theme--breakpoints--tablet: 1024px;
|
||||
--c--theme--logo--src: /assets/logo-gouv.svg;
|
||||
--c--theme--logo--widthheader: 110px;
|
||||
--c--theme--logo--widthfooter: 220px;
|
||||
--c--theme--logo--alt: gouvernement logo;
|
||||
--c--components--modal--width-small: 342px;
|
||||
--c--components--button--medium-height: 40px;
|
||||
--c--components--button--medium-text-height: 40px;
|
||||
--c--components--button--border-radius: 4px;
|
||||
--c--components--button--small-height: 26px;
|
||||
--c--components--button--primary--background--color: var(
|
||||
--c--theme--colors--primary-400
|
||||
);
|
||||
--c--components--button--primary--background--color-active: var(
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
--c--components--button--primary--border--color-active: transparent;
|
||||
--c--components--button--secondary--color: var(
|
||||
--c--theme--colors--primary-500
|
||||
);
|
||||
--c--components--button--secondary--color-hover: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--button--secondary--background--color: white;
|
||||
--c--components--button--secondary--background--color-hover: var(
|
||||
--c--theme--colors--primary-700
|
||||
--c--components--button--primary--background--color-hover: #1212ff;
|
||||
--c--components--button--primary--background--color-active: #2323ff;
|
||||
--c--components--button--primary--background--color-disabled: var(
|
||||
--c--theme--colors--greyscale-100
|
||||
);
|
||||
--c--components--button--primary--color: #fff;
|
||||
--c--components--button--primary--color-hover: #fff;
|
||||
--c--components--button--primary--color-active: #fff;
|
||||
--c--components--button--primary--color-focus-visible: #fff;
|
||||
--c--components--button--primary--disabled: var(
|
||||
--c--theme--colors--greyscale-500
|
||||
);
|
||||
--c--components--button--primary-text--background--color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--button--primary-text--background--color-hover: var(
|
||||
--c--theme--colors--greyscale-100
|
||||
);
|
||||
--c--components--button--primary-text--background--color-active: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--primary-text--background--color-focus-visible: #fff;
|
||||
--c--components--button--primary-text--background--color-disabled: var(
|
||||
--c--theme--colors--greyscale-000
|
||||
);
|
||||
--c--components--button--primary-text--color: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--button--primary-text--color-hover: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--button--primary-text--disabled: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--button--secondary--background--color-hover: #f6f6f6;
|
||||
--c--components--button--secondary--background--color-active: #ededed;
|
||||
--c--components--button--secondary--background--color-focus-visible: var(
|
||||
--c--theme--colors--greyscale-000
|
||||
);
|
||||
--c--components--button--secondary--background--disabled: var(
|
||||
--c--theme--colors--greyscale-000
|
||||
);
|
||||
--c--components--button--secondary--color: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--button--secondary--border--color: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--button--tertiary--color: var(
|
||||
--c--theme--colors--primary-text
|
||||
--c--components--button--secondary--border--color-hover: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--button--tertiary--color-disabled: var(
|
||||
--c--theme--colors--greyscale-600
|
||||
--c--components--button--secondary--border--color-disabled: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--button--secondary--disabled: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--button--tertiary--background--color: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--tertiary--background--color-focus-visible: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--tertiary--background--color-hover: var(
|
||||
--c--theme--colors--primary-300
|
||||
);
|
||||
--c--components--button--tertiary--background--color-active: var(
|
||||
--c--theme--colors--primary-100
|
||||
--c--theme--colors--primary-300
|
||||
);
|
||||
--c--components--button--tertiary--background--color-disabled: var(
|
||||
--c--components--button--tertiary--background--disabled: var(
|
||||
--c--theme--colors--primary-050
|
||||
);
|
||||
--c--components--button--tertiary--color: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--button--tertiary--disabled: var(
|
||||
--c--theme--colors--primary-300
|
||||
);
|
||||
--c--components--button--tertiary-text--background--color-hover: var(
|
||||
--c--theme--colors--greyscale-100
|
||||
);
|
||||
--c--components--button--tertiary-text--color-hover: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--button--tertiary-text--color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--button--danger--color-hover: white;
|
||||
--c--components--button--danger--background--color: var(
|
||||
--c--theme--colors--danger-600
|
||||
);
|
||||
--c--components--button--danger--background--color-hover: #ff2725;
|
||||
--c--components--button--danger--background--color-focus-visible: var(
|
||||
--c--theme--colors--danger-600
|
||||
);
|
||||
--c--components--button--danger--background--color-disabled: var(
|
||||
--c--theme--colors--greyscale-100
|
||||
);
|
||||
--c--components--button--danger--color-disabled: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--datagrid--header--color: var(
|
||||
--c--theme--colors--greyscale-600
|
||||
);
|
||||
--c--components--datagrid--header--size: 12px;
|
||||
--c--components--datagrid--header--weight: 500;
|
||||
--c--components--datagrid--body--background-color-hover: var(
|
||||
--c--theme--colors--greyscale-100
|
||||
);
|
||||
--c--components--forms-checkbox--border-radius: 4px;
|
||||
--c--components--forms-checkbox--border-color: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--forms-checkbox--background-color--hover: var(
|
||||
--c--theme--colors--greyscale-100
|
||||
);
|
||||
--c--components--forms-checkbox--border--color-disabled: var(
|
||||
--c--theme--colors--greyscale-200
|
||||
);
|
||||
--c--components--button--disabled--color: white;
|
||||
--c--components--button--disabled--background--color: #b3cef0;
|
||||
--c--components--la-gauffre--activated: false;
|
||||
--c--components--home-proconnect--activated: false;
|
||||
--c--components--forms-checkbox--border--color: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--forms-checkbox--background--disabled: var(
|
||||
--c--theme--colors--greyscale-200
|
||||
);
|
||||
--c--components--forms-checkbox--background--enable: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--forms-checkbox--check--disabled: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--forms-checkbox--check--enable: var(
|
||||
--c--theme--colors--greyscale-000
|
||||
);
|
||||
--c--components--forms-checkbox--color: var(--c--theme--colors--primary-text);
|
||||
--c--components--forms-checkbox--label--color: var(
|
||||
--c--theme--colors--greyscale-1000
|
||||
);
|
||||
--c--components--forms-checkbox--label--size: var(
|
||||
--c--theme--font--sizes--sm
|
||||
);
|
||||
--c--components--forms-checkbox--label--weight: 500;
|
||||
--c--components--forms-checkbox--text--color: var(
|
||||
--c--theme--colors--greyscale-600
|
||||
);
|
||||
--c--components--forms-checkbox--text--size: var(--c--theme--font--sizes--s);
|
||||
--c--components--forms-checkbox--text--weight: 400;
|
||||
--c--components--forms-checkbox--text--color-disabled: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--forms-labelledbox--label-color--small: var(
|
||||
--c--theme--colors--greyscale-950
|
||||
);
|
||||
--c--components--forms-labelledbox--label-color--small--disabled: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--forms-labelledbox--label-color--big: var(
|
||||
--c--theme--colors--greyscale-950
|
||||
);
|
||||
--c--components--forms-labelledbox--label-color--big--disabled: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--forms-radio--border-color: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--forms-radio--background-color: var(
|
||||
--c--theme--colors--greyscale-000
|
||||
);
|
||||
--c--components--forms-radio--accent-color: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--forms-radio--accent-color-disabled: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--forms-switch--border--color-disabled: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--forms-switch--border--color: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--forms-switch--handle-background-color: white;
|
||||
--c--components--forms-switch--handle-background-color--disabled: var(
|
||||
--c--theme--colors--greyscale-000
|
||||
);
|
||||
--c--components--forms-switch--rail-background-color--disabled: var(
|
||||
--c--theme--colors--greyscale-000
|
||||
);
|
||||
--c--components--forms-switch--accent-color: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--forms-textarea--label-color--focus: var(
|
||||
--c--theme--colors--greyscale-1000
|
||||
);
|
||||
--c--components--forms-textarea--border-radius: 4px;
|
||||
--c--components--forms-textarea--border-color: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--forms-textarea--box-shadow--color--hover: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--forms-textarea--box-shadow--color--focus: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--forms-textarea--value-color: var(
|
||||
--c--theme--colors--greyscale-950
|
||||
);
|
||||
--c--components--forms-textarea--value-color--disabled: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--forms-textarea--font-size: 14px;
|
||||
--c--components--forms-input--label-color--focus: var(
|
||||
--c--theme--colors--greyscale-1000
|
||||
);
|
||||
--c--components--forms-input--border-radius: 4px;
|
||||
--c--components--forms-input--border-color: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--forms-input--box-shadow--color--hover: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--forms-input--box-shadow--color--focus: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--forms-input--value-color: var(
|
||||
--c--theme--colors--greyscale-950
|
||||
);
|
||||
--c--components--forms-input--value-color--disabled: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--forms-input--font-size: 14px;
|
||||
--c--components--forms-select--label-color--focus: var(
|
||||
--c--theme--colors--greyscale-1000
|
||||
);
|
||||
--c--components--forms-select--item-font-size: 14px;
|
||||
--c--components--forms-select--border-radius: 4px;
|
||||
--c--components--forms-select--border-radius-hover: 4px;
|
||||
--c--components--forms-select--border-color: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--forms-select--box-shadow--color--hover: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
);
|
||||
--c--components--forms-select--box-shadow--color--focus: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--forms-select--value-color: var(
|
||||
--c--theme--colors--greyscale-950
|
||||
);
|
||||
--c--components--forms-select--font-size: 14px;
|
||||
--c--components--la-gauffre--activated: true;
|
||||
--c--components--home-proconnect--activated: true;
|
||||
}
|
||||
|
||||
.cunningham-theme--dark {
|
||||
@@ -397,219 +501,6 @@
|
||||
--c--theme--colors--danger-900: #9d6666;
|
||||
}
|
||||
|
||||
.cunningham-theme--dsfr {
|
||||
--c--theme--colors--card-border: #e5e5e5;
|
||||
--c--theme--colors--primary-text: #000091;
|
||||
--c--theme--colors--primary-100: #ececfe;
|
||||
--c--theme--colors--primary-150: #f4f4fd;
|
||||
--c--theme--colors--primary-200: #e3e3fd;
|
||||
--c--theme--colors--primary-300: #cacafb;
|
||||
--c--theme--colors--primary-400: #8585f6;
|
||||
--c--theme--colors--primary-500: #6a6af4;
|
||||
--c--theme--colors--primary-600: #313178;
|
||||
--c--theme--colors--primary-700: #272747;
|
||||
--c--theme--colors--primary-800: #000091;
|
||||
--c--theme--colors--primary-900: #21213f;
|
||||
--c--theme--colors--secondary-text: #fff;
|
||||
--c--theme--colors--secondary-100: #fee9ea;
|
||||
--c--theme--colors--secondary-200: #fedfdf;
|
||||
--c--theme--colors--secondary-300: #fdbfbf;
|
||||
--c--theme--colors--secondary-400: #e1020f;
|
||||
--c--theme--colors--secondary-500: #c91a1f;
|
||||
--c--theme--colors--secondary-600: #5e2b2b;
|
||||
--c--theme--colors--secondary-700: #3b2424;
|
||||
--c--theme--colors--secondary-800: #341f1f;
|
||||
--c--theme--colors--secondary-900: #2b1919;
|
||||
--c--theme--colors--greyscale-text: #303c4b;
|
||||
--c--theme--colors--greyscale-000: #fff;
|
||||
--c--theme--colors--greyscale-050: #f6f6f6;
|
||||
--c--theme--colors--greyscale-100: #eee;
|
||||
--c--theme--colors--greyscale-200: #e5e5e5;
|
||||
--c--theme--colors--greyscale-250: #ddd;
|
||||
--c--theme--colors--greyscale-300: #cecece;
|
||||
--c--theme--colors--greyscale-350: #ddd;
|
||||
--c--theme--colors--greyscale-400: #929292;
|
||||
--c--theme--colors--greyscale-500: #7c7c7c;
|
||||
--c--theme--colors--greyscale-600: #666;
|
||||
--c--theme--colors--greyscale-700: #3a3a3a;
|
||||
--c--theme--colors--greyscale-750: #353535;
|
||||
--c--theme--colors--greyscale-800: #2a2a2a;
|
||||
--c--theme--colors--greyscale-900: #242424;
|
||||
--c--theme--colors--greyscale-950: #1e1e1e;
|
||||
--c--theme--colors--greyscale-1000: #161616;
|
||||
--c--theme--colors--success-text: #1f8d49;
|
||||
--c--theme--colors--success-100: #dffee6;
|
||||
--c--theme--colors--success-200: #b8fec9;
|
||||
--c--theme--colors--success-300: #88fdaa;
|
||||
--c--theme--colors--success-400: #3bea7e;
|
||||
--c--theme--colors--success-500: #1f8d49;
|
||||
--c--theme--colors--success-600: #18753c;
|
||||
--c--theme--colors--success-700: #204129;
|
||||
--c--theme--colors--success-800: #1e2e22;
|
||||
--c--theme--colors--success-900: #19281d;
|
||||
--c--theme--colors--info-text: #0078f3;
|
||||
--c--theme--colors--info-100: #e8edff;
|
||||
--c--theme--colors--info-200: #dde5ff;
|
||||
--c--theme--colors--info-300: #bccdff;
|
||||
--c--theme--colors--info-400: #518fff;
|
||||
--c--theme--colors--info-500: #0078f3;
|
||||
--c--theme--colors--info-600: #0063cb;
|
||||
--c--theme--colors--info-700: #273961;
|
||||
--c--theme--colors--info-800: #222a3f;
|
||||
--c--theme--colors--info-900: #1d2437;
|
||||
--c--theme--colors--warning-text: #d64d00;
|
||||
--c--theme--colors--warning-100: #fff4f3;
|
||||
--c--theme--colors--warning-200: #ffe9e6;
|
||||
--c--theme--colors--warning-300: #ffded9;
|
||||
--c--theme--colors--warning-400: #ffbeb4;
|
||||
--c--theme--colors--warning-500: #d64d00;
|
||||
--c--theme--colors--warning-600: #b34000;
|
||||
--c--theme--colors--warning-700: #5e2c21;
|
||||
--c--theme--colors--warning-800: #3e241e;
|
||||
--c--theme--colors--warning-900: #361e19;
|
||||
--c--theme--colors--danger-text: #fff;
|
||||
--c--theme--colors--danger-100: #ffe9e9;
|
||||
--c--theme--colors--danger-200: #fdd;
|
||||
--c--theme--colors--danger-300: #ffbdbd;
|
||||
--c--theme--colors--danger-400: #ff5655;
|
||||
--c--theme--colors--danger-500: #f60700;
|
||||
--c--theme--colors--danger-600: #ce0500;
|
||||
--c--theme--colors--danger-700: #642626;
|
||||
--c--theme--colors--danger-800: #412121;
|
||||
--c--theme--colors--danger-900: #391c1c;
|
||||
--c--theme--font--families--accent: marianne;
|
||||
--c--theme--font--families--base: marianne;
|
||||
--c--theme--logo--src: /assets/logo-gouv.svg;
|
||||
--c--theme--logo--widthHeader: 110px;
|
||||
--c--theme--logo--widthFooter: 220px;
|
||||
--c--theme--logo--alt: gouvernement logo;
|
||||
--c--components--alert--border-radius: 0;
|
||||
--c--components--alert--error--background-color: var(
|
||||
--c--theme--colors--danger-100
|
||||
);
|
||||
--c--components--alert--error--border-left-color: var(
|
||||
--c--theme--colors--danger-400
|
||||
);
|
||||
--c--components--alert--error--close--color: white;
|
||||
--c--components--alert--error--close--background-color: var(
|
||||
--c--theme--colors--danger-400
|
||||
);
|
||||
--c--components--alert--error--close--background-color-hover: var(
|
||||
--c--theme--colors--danger-600
|
||||
);
|
||||
--c--components--modal--width-small: 342px;
|
||||
--c--components--button--medium-height: 40px;
|
||||
--c--components--button--medium-text-height: 40px;
|
||||
--c--components--button--border-radius: 4px;
|
||||
--c--components--button--primary--background--color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--button--primary--background--color-hover: #1212ff;
|
||||
--c--components--button--primary--background--color-active: #2323ff;
|
||||
--c--components--button--primary--color: #fff;
|
||||
--c--components--button--primary--color-hover: #fff;
|
||||
--c--components--button--primary--color-active: #fff;
|
||||
--c--components--button--primary-text--background--color-hover: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--primary-text--background--color-active: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--primary-text--color-hover: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--button--primary-text--color: var(
|
||||
--c--theme--colors--primary-800
|
||||
);
|
||||
--c--components--button--secondary--background--color-hover: #f6f6f6;
|
||||
--c--components--button--secondary--background--color-active: #ededed;
|
||||
--c--components--button--secondary--border--color: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--button--secondary--border--color-hover: var(
|
||||
--c--theme--colors--greyscale-300
|
||||
);
|
||||
--c--components--button--secondary--color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--button--tertiary-text--background--color-hover: var(
|
||||
--c--theme--colors--greyscale-100
|
||||
);
|
||||
--c--components--button--tertiary-text--color-hover: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--button--tertiary-text--color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--datagrid--header--color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--datagrid--header--size: var(--c--theme--font--sizes--s);
|
||||
--c--components--datagrid--body--background-color: transparent;
|
||||
--c--components--datagrid--body--background-color-hover: #f4f4fd;
|
||||
--c--components--datagrid--pagination--background-color: transparent;
|
||||
--c--components--datagrid--pagination--background-color-active: var(
|
||||
--c--theme--colors--primary-300
|
||||
);
|
||||
--c--components--datagrid--pagination--border-color: var(
|
||||
--c--theme--colors--primary-400
|
||||
);
|
||||
--c--components--forms-checkbox--border-radius: 0;
|
||||
--c--components--forms-checkbox--color: var(--c--theme--colors--primary-text);
|
||||
--c--components--forms-checkbox--text--color: var(
|
||||
--c--theme--colors--greyscale-text
|
||||
);
|
||||
--c--components--forms-checkbox--text--size: var(--c--theme--font--sizes--t);
|
||||
--c--components--forms-datepicker--border-radius: 0;
|
||||
--c--components--forms-fileuploader--border-radius: 0;
|
||||
--c--components--forms-field--color: var(--c--theme--colors--primary-text);
|
||||
--c--components--forms-field--footer-font-size: var(
|
||||
--c--theme--font--sizes--t
|
||||
);
|
||||
--c--components--forms-field--footer-color: var(
|
||||
--c--theme--colors--greyscale-text
|
||||
);
|
||||
--c--components--forms-input--border-radius: 4px;
|
||||
--c--components--forms-input--background-color: #fff;
|
||||
--c--components--forms-input--border-color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--forms-input--box-shadow-color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--forms-input--value-color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--forms-input--font-size: 14px;
|
||||
--c--components--forms-labelledbox--label-color--big: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--forms-radio--accent-color: var(
|
||||
--c--theme--colors--primary-600
|
||||
);
|
||||
--c--components--forms-select--item-font-size: 14px;
|
||||
--c--components--forms-select--border-radius: 4px;
|
||||
--c--components--forms-select--border-radius-hover: 4px;
|
||||
--c--components--forms-select--background-color: #fff;
|
||||
--c--components--forms-select--border-color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--forms-select--border-color-hover: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--forms-select--box-shadow-color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--forms-switch--handle-border-radius: 2px;
|
||||
--c--components--forms-switch--rail-border-radius: 4px;
|
||||
--c--components--forms-switch--accent-color: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--forms-textarea--border-radius: 0;
|
||||
--c--components--la-gauffre--activated: true;
|
||||
--c--components--home-proconnect--activated: true;
|
||||
}
|
||||
|
||||
.clr-secondary-text {
|
||||
color: var(--c--theme--colors--secondary-text);
|
||||
}
|
||||
@@ -890,18 +781,6 @@
|
||||
color: var(--c--theme--colors--danger-text);
|
||||
}
|
||||
|
||||
.clr-card-border {
|
||||
color: var(--c--theme--colors--card-border);
|
||||
}
|
||||
|
||||
.clr-primary-bg {
|
||||
color: var(--c--theme--colors--primary-bg);
|
||||
}
|
||||
|
||||
.clr-primary-action {
|
||||
color: var(--c--theme--colors--primary-action);
|
||||
}
|
||||
|
||||
.clr-primary-050 {
|
||||
color: var(--c--theme--colors--primary-050);
|
||||
}
|
||||
@@ -910,18 +789,42 @@
|
||||
color: var(--c--theme--colors--primary-150);
|
||||
}
|
||||
|
||||
.clr-primary-950 {
|
||||
color: var(--c--theme--colors--primary-950);
|
||||
.clr-greyscale-text {
|
||||
color: var(--c--theme--colors--greyscale-text);
|
||||
}
|
||||
|
||||
.clr-info-150 {
|
||||
color: var(--c--theme--colors--info-150);
|
||||
.clr-greyscale-050 {
|
||||
color: var(--c--theme--colors--greyscale-050);
|
||||
}
|
||||
|
||||
.clr-greyscale-250 {
|
||||
color: var(--c--theme--colors--greyscale-250);
|
||||
}
|
||||
|
||||
.clr-greyscale-350 {
|
||||
color: var(--c--theme--colors--greyscale-350);
|
||||
}
|
||||
|
||||
.clr-greyscale-750 {
|
||||
color: var(--c--theme--colors--greyscale-750);
|
||||
}
|
||||
|
||||
.clr-greyscale-950 {
|
||||
color: var(--c--theme--colors--greyscale-950);
|
||||
}
|
||||
|
||||
.clr-greyscale-1000 {
|
||||
color: var(--c--theme--colors--greyscale-1000);
|
||||
}
|
||||
|
||||
.clr-primary-action {
|
||||
color: var(--c--theme--colors--primary-action);
|
||||
}
|
||||
|
||||
.clr-primary-bg {
|
||||
color: var(--c--theme--colors--primary-bg);
|
||||
}
|
||||
|
||||
.clr-blue-400 {
|
||||
color: var(--c--theme--colors--blue-400);
|
||||
}
|
||||
@@ -1322,18 +1225,6 @@
|
||||
background-color: var(--c--theme--colors--danger-text);
|
||||
}
|
||||
|
||||
.bg-card-border {
|
||||
background-color: var(--c--theme--colors--card-border);
|
||||
}
|
||||
|
||||
.bg-primary-bg {
|
||||
background-color: var(--c--theme--colors--primary-bg);
|
||||
}
|
||||
|
||||
.bg-primary-action {
|
||||
background-color: var(--c--theme--colors--primary-action);
|
||||
}
|
||||
|
||||
.bg-primary-050 {
|
||||
background-color: var(--c--theme--colors--primary-050);
|
||||
}
|
||||
@@ -1342,18 +1233,42 @@
|
||||
background-color: var(--c--theme--colors--primary-150);
|
||||
}
|
||||
|
||||
.bg-primary-950 {
|
||||
background-color: var(--c--theme--colors--primary-950);
|
||||
.bg-greyscale-text {
|
||||
background-color: var(--c--theme--colors--greyscale-text);
|
||||
}
|
||||
|
||||
.bg-info-150 {
|
||||
background-color: var(--c--theme--colors--info-150);
|
||||
.bg-greyscale-050 {
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
}
|
||||
|
||||
.bg-greyscale-250 {
|
||||
background-color: var(--c--theme--colors--greyscale-250);
|
||||
}
|
||||
|
||||
.bg-greyscale-350 {
|
||||
background-color: var(--c--theme--colors--greyscale-350);
|
||||
}
|
||||
|
||||
.bg-greyscale-750 {
|
||||
background-color: var(--c--theme--colors--greyscale-750);
|
||||
}
|
||||
|
||||
.bg-greyscale-950 {
|
||||
background-color: var(--c--theme--colors--greyscale-950);
|
||||
}
|
||||
|
||||
.bg-greyscale-1000 {
|
||||
background-color: var(--c--theme--colors--greyscale-1000);
|
||||
}
|
||||
|
||||
.bg-primary-action {
|
||||
background-color: var(--c--theme--colors--primary-action);
|
||||
}
|
||||
|
||||
.bg-primary-bg {
|
||||
background-color: var(--c--theme--colors--primary-bg);
|
||||
}
|
||||
|
||||
.bg-blue-400 {
|
||||
background-color: var(--c--theme--colors--blue-400);
|
||||
}
|
||||
|
||||
@@ -3,84 +3,87 @@ export const tokens = {
|
||||
default: {
|
||||
theme: {
|
||||
colors: {
|
||||
'secondary-text': '#555F6B',
|
||||
'secondary-100': '#F2F7FC',
|
||||
'secondary-200': '#EBF3FA',
|
||||
'secondary-300': '#E2EEF8',
|
||||
'secondary-400': '#DDEAF7',
|
||||
'secondary-500': '#D4E5F5',
|
||||
'secondary-600': '#C1D0DF',
|
||||
'secondary-700': '#97A3AE',
|
||||
'secondary-800': '#757E87',
|
||||
'secondary-900': '#596067',
|
||||
'info-text': '#fff',
|
||||
'info-100': '#EBF2FC',
|
||||
'info-200': '#8CB5EA',
|
||||
'info-300': '#5894E1',
|
||||
'info-400': '#377FDB',
|
||||
'info-500': '#055FD2',
|
||||
'info-600': '#0556BF',
|
||||
'info-700': '#044395',
|
||||
'info-800': '#033474',
|
||||
'info-900': '#022858',
|
||||
'greyscale-100': '#FAFAFB',
|
||||
'greyscale-200': '#F3F4F4',
|
||||
'greyscale-300': '#E7E8EA',
|
||||
'greyscale-400': '#C2C6CA',
|
||||
'greyscale-500': '#9EA3AA',
|
||||
'greyscale-600': '#79818A',
|
||||
'greyscale-700': '#555F6B',
|
||||
'greyscale-800': '#303C4B',
|
||||
'greyscale-900': '#0C1A2B',
|
||||
'secondary-text': '#fff',
|
||||
'secondary-100': '#fee9ea',
|
||||
'secondary-200': '#fedfdf',
|
||||
'secondary-300': '#fdbfbf',
|
||||
'secondary-400': '#e1020f',
|
||||
'secondary-500': '#c91a1f',
|
||||
'secondary-600': '#5e2b2b',
|
||||
'secondary-700': '#3b2424',
|
||||
'secondary-800': '#341f1f',
|
||||
'secondary-900': '#2b1919',
|
||||
'info-text': '#0078f3',
|
||||
'info-100': '#E8EDFF',
|
||||
'info-200': '#DDE5FF',
|
||||
'info-300': '#BCCDFF',
|
||||
'info-400': '#518FFF',
|
||||
'info-500': '#0078F3',
|
||||
'info-600': '#0063CB',
|
||||
'info-700': '#273961',
|
||||
'info-800': '#222A3F',
|
||||
'info-900': '#1D2437',
|
||||
'greyscale-100': '#eee',
|
||||
'greyscale-200': '#E5E5E5',
|
||||
'greyscale-300': '#CECECE',
|
||||
'greyscale-400': '#929292',
|
||||
'greyscale-500': '#7C7C7C',
|
||||
'greyscale-600': '#666666',
|
||||
'greyscale-700': '#3A3A3A',
|
||||
'greyscale-800': '#2A2A2A',
|
||||
'greyscale-900': '#242424',
|
||||
'greyscale-000': '#fff',
|
||||
'primary-100': '#EDF5FA',
|
||||
'primary-200': '#8CB5EA',
|
||||
'primary-300': '#5894E1',
|
||||
'primary-400': '#377FDB',
|
||||
'primary-500': '#055FD2',
|
||||
'primary-600': '#0556BF',
|
||||
'primary-700': '#044395',
|
||||
'primary-800': '#033474',
|
||||
'primary-900': '#022858',
|
||||
'success-100': '#EFFCD3',
|
||||
'success-200': '#DBFAA9',
|
||||
'success-300': '#BEF27C',
|
||||
'success-400': '#A0E659',
|
||||
'success-500': '#76D628',
|
||||
'success-600': '#5AB81D',
|
||||
'success-700': '#419A14',
|
||||
'success-800': '#2C7C0C',
|
||||
'success-900': '#1D6607',
|
||||
'warning-100': '#FFF8CD',
|
||||
'warning-200': '#FFEF9B',
|
||||
'warning-300': '#FFE469',
|
||||
'warning-400': '#FFDA43',
|
||||
'warning-500': '#FFC805',
|
||||
'warning-600': '#DBA603',
|
||||
'warning-700': '#B78702',
|
||||
'warning-800': '#936901',
|
||||
'warning-900': '#7A5400',
|
||||
'danger-100': '#F4B0B0',
|
||||
'danger-200': '#EE8A8A',
|
||||
'danger-300': '#E65454',
|
||||
'danger-400': '#E13333',
|
||||
'danger-500': '#DA0000',
|
||||
'danger-600': '#C60000',
|
||||
'danger-700': '#9B0000',
|
||||
'danger-800': '#780000',
|
||||
'danger-900': '#5C0000',
|
||||
'primary-text': '#fff',
|
||||
'success-text': '#fff',
|
||||
'warning-text': '#fff',
|
||||
'danger-text': '#fff',
|
||||
'card-border': '#ededed',
|
||||
'primary-bg': '#FAFAFA',
|
||||
'primary-action': '#1212FF',
|
||||
'primary-100': '#ECECFE',
|
||||
'primary-200': '#E3E3FD',
|
||||
'primary-300': '#CACAFB',
|
||||
'primary-400': '#8585F6',
|
||||
'primary-500': '#6A6AF4',
|
||||
'primary-600': '#313178',
|
||||
'primary-700': '#272747',
|
||||
'primary-800': '#000091',
|
||||
'primary-900': '#21213F',
|
||||
'success-100': '#dffee6',
|
||||
'success-200': '#b8fec9',
|
||||
'success-300': '#88fdaa',
|
||||
'success-400': '#3bea7e',
|
||||
'success-500': '#1f8d49',
|
||||
'success-600': '#18753c',
|
||||
'success-700': '#204129',
|
||||
'success-800': '#1e2e22',
|
||||
'success-900': '#19281d',
|
||||
'warning-100': '#fff4f3',
|
||||
'warning-200': '#ffe9e6',
|
||||
'warning-300': '#ffded9',
|
||||
'warning-400': '#ffbeb4',
|
||||
'warning-500': '#d64d00',
|
||||
'warning-600': '#b34000',
|
||||
'warning-700': '#5e2c21',
|
||||
'warning-800': '#3e241e',
|
||||
'warning-900': '#361e19',
|
||||
'danger-100': '#FFE9E9',
|
||||
'danger-200': '#FFDDDD',
|
||||
'danger-300': '#FFBDBD',
|
||||
'danger-400': '#FF5655',
|
||||
'danger-500': '#F60700',
|
||||
'danger-600': '#CE0500',
|
||||
'danger-700': '#642626',
|
||||
'danger-800': '#412121',
|
||||
'danger-900': '#391C1C',
|
||||
'primary-text': '#000091',
|
||||
'success-text': '#1f8d49',
|
||||
'warning-text': '#d64d00',
|
||||
'danger-text': '#FFF',
|
||||
'primary-050': '#F5F5FE',
|
||||
'primary-150': '#E5EEFA',
|
||||
'primary-950': '#1B1B35',
|
||||
'info-150': '#E5EEFA',
|
||||
'primary-150': '#F4F4FD',
|
||||
'greyscale-text': '#303C4B',
|
||||
'greyscale-050': '#F6F6F6',
|
||||
'greyscale-250': '#ddd',
|
||||
'greyscale-350': '#ddd',
|
||||
'greyscale-750': '#353535',
|
||||
'greyscale-950': '#1E1E1E',
|
||||
'greyscale-1000': '#161616',
|
||||
'primary-action': '#1212FF',
|
||||
'primary-bg': '#FAFAFA',
|
||||
'blue-400': '#7AB1E8',
|
||||
'blue-500': '#417DC4',
|
||||
'blue-600': '#3558A2',
|
||||
@@ -145,10 +148,7 @@ export const tokens = {
|
||||
extrabold: 800,
|
||||
black: 900,
|
||||
},
|
||||
families: {
|
||||
base: '"Roboto Flex Variable", sans-serif',
|
||||
accent: '"Roboto Flex Variable", sans-serif',
|
||||
},
|
||||
families: { base: 'Marianne', accent: 'Marianne' },
|
||||
letterSpacings: {
|
||||
h1: 'normal',
|
||||
h2: 'normal',
|
||||
@@ -202,141 +202,164 @@ export const tokens = {
|
||||
xl: '1200px',
|
||||
xxl: '1400px',
|
||||
xxs: '320px',
|
||||
mobile: '768px',
|
||||
tablet: '1024px',
|
||||
},
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
logo: { src: '', widthHeader: '', widthFooter: '', alt: '' },
|
||||
},
|
||||
components: {
|
||||
datagrid: {
|
||||
header: {
|
||||
weight: 'var(--c--theme--font--weights--extrabold)',
|
||||
size: 'var(--c--theme--font--sizes--ml)',
|
||||
},
|
||||
cell: {
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
size: 'var(--c--theme--font--sizes--ml)',
|
||||
},
|
||||
},
|
||||
'forms-checkbox': {
|
||||
'background-color': { hover: '#055fd214' },
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
'font-size': 'var(--c--theme--font--sizes--ml)',
|
||||
},
|
||||
'forms-datepicker': {
|
||||
'border-color': 'var(--c--theme--colors--primary-500)',
|
||||
'value-color': 'var(--c--theme--colors--primary-500)',
|
||||
'border-radius': {
|
||||
hover: 'var(--c--components--forms-datepicker--border-radius)',
|
||||
focus: 'var(--c--components--forms-datepicker--border-radius)',
|
||||
},
|
||||
},
|
||||
'forms-field': {
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
'value-color': 'var(--c--theme--colors--primary-500)',
|
||||
width: 'auto',
|
||||
},
|
||||
'forms-input': {
|
||||
'value-color': 'var(--c--theme--colors--primary-500)',
|
||||
'border-color': 'var(--c--theme--colors--primary-500)',
|
||||
color: {
|
||||
error: 'var(--c--theme--colors--danger-500)',
|
||||
'error-hover': 'var(--c--theme--colors--danger-500)',
|
||||
'box-shadow-error-hover': 'var(--c--theme--colors--danger-500)',
|
||||
},
|
||||
},
|
||||
'forms-labelledbox': {
|
||||
'label-color': {
|
||||
small: 'var(--c--theme--colors--primary-500)',
|
||||
'small-disabled': 'var(--c--theme--colors--greyscale-400)',
|
||||
big: { disabled: 'var(--c--theme--colors--greyscale-400)' },
|
||||
},
|
||||
},
|
||||
'forms-select': {
|
||||
'border-color': 'var(--c--theme--colors--primary-500)',
|
||||
'border-color-disabled-hover':
|
||||
'var(--c--theme--colors--greyscale-200)',
|
||||
'border-radius': {
|
||||
hover: 'var(--c--components--forms-select--border-radius)',
|
||||
focus: 'var(--c--components--forms-select--border-radius)',
|
||||
},
|
||||
'font-size': 'var(--c--theme--font--sizes--ml)',
|
||||
'menu-background-color': '#fff',
|
||||
'item-background-color': {
|
||||
hover: 'var(--c--theme--colors--primary-300)',
|
||||
},
|
||||
},
|
||||
'forms-switch': {
|
||||
'accent-color': 'var(--c--theme--colors--primary-400)',
|
||||
},
|
||||
'forms-textarea': {
|
||||
'border-color': 'var(--c--components--forms-textarea--border-color)',
|
||||
'border-color-hover':
|
||||
'var(--c--components--forms-textarea--border-color)',
|
||||
'border-radius': {
|
||||
hover: 'var(--c--components--forms-textarea--border-radius)',
|
||||
focus: 'var(--c--components--forms-textarea--border-radius)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
disabled: {
|
||||
'border-color-hover': 'var(--c--theme--colors--greyscale-200)',
|
||||
},
|
||||
},
|
||||
modal: { 'background-color': '#fff' },
|
||||
modal: { 'width-small': '342px' },
|
||||
button: {
|
||||
'border-radius': {
|
||||
active: 'var(--c--components--button--border-radius)',
|
||||
'medium-height': '40px',
|
||||
'medium-text-height': '40px',
|
||||
'border-radius': '4px',
|
||||
'small-height': '26px',
|
||||
primary: {
|
||||
'background--color': 'var(--c--theme--colors--primary-text)',
|
||||
'background--color-hover': '#1212ff',
|
||||
'background--color-active': '#2323ff',
|
||||
'background--color-disabled':
|
||||
'var(--c--theme--colors--greyscale-100)',
|
||||
color: '#fff',
|
||||
'color-hover': '#fff',
|
||||
'color-active': '#fff',
|
||||
'color-focus-visible': '#fff',
|
||||
disabled: 'var(--c--theme--colors--greyscale-500)',
|
||||
},
|
||||
'medium-height': 'auto',
|
||||
'small-height': 'auto',
|
||||
success: {
|
||||
color: 'white',
|
||||
'color-disabled': 'white',
|
||||
'color-hover': 'white',
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--success-600)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-300)',
|
||||
'color-hover': 'var(--c--theme--colors--success-800)',
|
||||
},
|
||||
'primary-text': {
|
||||
'background--color': 'var(--c--theme--colors--primary-text)',
|
||||
'background--color-hover': 'var(--c--theme--colors--greyscale-100)',
|
||||
'background--color-active': 'var(--c--theme--colors--primary-100)',
|
||||
'background--color-focus-visible': '#fff',
|
||||
'background--color-disabled':
|
||||
'var(--c--theme--colors--greyscale-000)',
|
||||
color: 'var(--c--theme--colors--primary-800)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-800)',
|
||||
disabled: 'var(--c--theme--colors--greyscale-400)',
|
||||
},
|
||||
secondary: {
|
||||
'background--color-hover': '#F6F6F6',
|
||||
'background--color-active': '#EDEDED',
|
||||
'background--color-focus-visible':
|
||||
'var(--c--theme--colors--greyscale-000)',
|
||||
'background--disabled': 'var(--c--theme--colors--greyscale-000)',
|
||||
color: 'var(--c--theme--colors--primary-800)',
|
||||
'border--color': 'var(--c--theme--colors--greyscale-300)',
|
||||
'border--color-hover': 'var(--c--theme--colors--greyscale-300)',
|
||||
'border--color-disabled': 'var(--c--theme--colors--greyscale-300)',
|
||||
disabled: 'var(--c--theme--colors--greyscale-400)',
|
||||
},
|
||||
tertiary: {
|
||||
'background--color': 'var(--c--theme--colors--primary-100)',
|
||||
'background--color-focus-visible':
|
||||
'var(--c--theme--colors--primary-100)',
|
||||
'background--color-hover': 'var(--c--theme--colors--primary-300)',
|
||||
'background--color-active': 'var(--c--theme--colors--primary-300)',
|
||||
'background--disabled': 'var(--c--theme--colors--primary-050)',
|
||||
color: 'var(--c--theme--colors--primary-800)',
|
||||
disabled: 'var(--c--theme--colors--primary-300)',
|
||||
},
|
||||
'tertiary-text': {
|
||||
'background--color-hover': 'var(--c--theme--colors--greyscale-100)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
danger: {
|
||||
'color-hover': 'white',
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--danger-600)',
|
||||
'color-hover': '#FF2725',
|
||||
'color-disabled': 'var(--c--theme--colors--danger-100)',
|
||||
},
|
||||
'background--color': 'var(--c--theme--colors--danger-600)',
|
||||
'background--color-hover': '#FF2725',
|
||||
'background--color-focus-visible':
|
||||
'var(--c--theme--colors--danger-600)',
|
||||
'background--color-disabled':
|
||||
'var(--c--theme--colors--greyscale-100)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-400)',
|
||||
},
|
||||
primary: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'color-active': 'var(--c--theme--colors--primary-text)',
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--primary-400)',
|
||||
'color-active': 'var(--c--theme--colors--primary-500)',
|
||||
},
|
||||
border: { 'color-active': 'transparent' },
|
||||
},
|
||||
secondary: {
|
||||
color: 'var(--c--theme--colors--primary-500)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
background: {
|
||||
color: 'white',
|
||||
'color-hover': 'var(--c--theme--colors--primary-700)',
|
||||
},
|
||||
border: { color: 'var(--c--theme--colors--greyscale-300)' },
|
||||
},
|
||||
tertiary: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--primary-100)',
|
||||
'color-hover': 'var(--c--theme--colors--primary-300)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
|
||||
},
|
||||
},
|
||||
disabled: { color: 'white', background: { color: '#b3cef0' } },
|
||||
},
|
||||
'la-gauffre': { activated: false },
|
||||
'home-proconnect': { activated: false },
|
||||
datagrid: {
|
||||
'header--color': '#666666',
|
||||
'header--size': '12px',
|
||||
'header--weight': '500',
|
||||
'body--background-color-hover': '#eee',
|
||||
},
|
||||
'forms-checkbox': {
|
||||
'border-radius': '4px',
|
||||
'border-color': 'var(--c--theme--colors--primary-800)',
|
||||
'background-color--hover': 'var(--c--theme--colors--greyscale-100)',
|
||||
'border--color-disabled': 'var(--c--theme--colors--greyscale-200)',
|
||||
'border--color': 'var(--c--theme--colors--primary-800)',
|
||||
'background--disabled': 'var(--c--theme--colors--greyscale-200)',
|
||||
'background--enable': 'var(--c--theme--colors--primary-800)',
|
||||
'check--disabled': 'var(--c--theme--colors--greyscale-300)',
|
||||
'check--enable': 'var(--c--theme--colors--greyscale-000)',
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'label--color': 'var(--c--theme--colors--greyscale-1000)',
|
||||
'label--size': 'var(--c--theme--font--sizes--sm)',
|
||||
'label--weight': '500',
|
||||
'text--color': 'var(--c--theme--colors--greyscale-600)',
|
||||
'text--size': 'var(--c--theme--font--sizes--s)',
|
||||
'text--weight': '400',
|
||||
'text--color-disabled': 'var(--c--theme--colors--greyscale-300)',
|
||||
},
|
||||
'forms-labelledbox': {
|
||||
'label-color--small': '#1E1E1E',
|
||||
'label-color--small--disabled': '#CECECE',
|
||||
'label-color--big': '#1E1E1E',
|
||||
'label-color--big--disabled': '#CECECE',
|
||||
},
|
||||
'forms-radio': {
|
||||
'border-color': 'var(--c--theme--colors--primary-800)',
|
||||
'background-color': 'var(--c--theme--colors--greyscale-000)',
|
||||
'accent-color': 'var(--c--theme--colors--primary-800)',
|
||||
'accent-color-disabled': 'var(--c--theme--colors--greyscale-300)',
|
||||
},
|
||||
'forms-switch': {
|
||||
'border--color-disabled': 'var(--c--theme--colors--greyscale-300)',
|
||||
'border--color': 'var(--c--theme--colors--primary-800)',
|
||||
'handle-background-color': 'white',
|
||||
'handle-background-color--disabled':
|
||||
'var(--c--theme--colors--greyscale-000)',
|
||||
'rail-background-color--disabled':
|
||||
'var(--c--theme--colors--greyscale-000)',
|
||||
'accent-color': 'var(--c--theme--colors--primary-800)',
|
||||
},
|
||||
'forms-textarea': {
|
||||
'label-color--focus': '#161616',
|
||||
'border-radius': '4px',
|
||||
'border-color': '#929292',
|
||||
'box-shadow--color--hover': '#929292',
|
||||
'box-shadow--color--focus': '#000091',
|
||||
'value-color': '#1E1E1E',
|
||||
'value-color--disabled': '#CECECE',
|
||||
'font-size': '14px',
|
||||
},
|
||||
'forms-input': {
|
||||
'label-color--focus': '#161616',
|
||||
'border-radius': '4px',
|
||||
'border-color': '#929292',
|
||||
'box-shadow--color--hover': '#929292',
|
||||
'box-shadow--color--focus': '#000091',
|
||||
'value-color': '#1E1E1E',
|
||||
'value-color--disabled': '#CECECE',
|
||||
'font-size': '14px',
|
||||
},
|
||||
'forms-select': {
|
||||
'label-color--focus': '#161616',
|
||||
'item-font-size': '14px',
|
||||
'border-radius': '4px',
|
||||
'border-radius-hover': '4px',
|
||||
'border-color': '#929292',
|
||||
'box-shadow--color--hover': '#929292',
|
||||
'box-shadow--color--focus': '#000091',
|
||||
'value-color': '#1E1E1E',
|
||||
'font-size': '14px',
|
||||
},
|
||||
'la-gauffre': { activated: true },
|
||||
'home-proconnect': { activated: true },
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
@@ -391,211 +414,5 @@ export const tokens = {
|
||||
},
|
||||
},
|
||||
},
|
||||
dsfr: {
|
||||
theme: {
|
||||
colors: {
|
||||
'card-border': '#E5E5E5',
|
||||
'primary-text': '#000091',
|
||||
'primary-100': '#ECECFE',
|
||||
'primary-150': '#F4F4FD',
|
||||
'primary-200': '#E3E3FD',
|
||||
'primary-300': '#CACAFB',
|
||||
'primary-400': '#8585F6',
|
||||
'primary-500': '#6A6AF4',
|
||||
'primary-600': '#313178',
|
||||
'primary-700': '#272747',
|
||||
'primary-800': '#000091',
|
||||
'primary-900': '#21213F',
|
||||
'secondary-text': '#fff',
|
||||
'secondary-100': '#fee9ea',
|
||||
'secondary-200': '#fedfdf',
|
||||
'secondary-300': '#fdbfbf',
|
||||
'secondary-400': '#e1020f',
|
||||
'secondary-500': '#c91a1f',
|
||||
'secondary-600': '#5e2b2b',
|
||||
'secondary-700': '#3b2424',
|
||||
'secondary-800': '#341f1f',
|
||||
'secondary-900': '#2b1919',
|
||||
'greyscale-text': '#303C4B',
|
||||
'greyscale-000': '#fff',
|
||||
'greyscale-050': '#F6F6F6',
|
||||
'greyscale-100': '#eee',
|
||||
'greyscale-200': '#E5E5E5',
|
||||
'greyscale-250': '#ddd',
|
||||
'greyscale-300': '#CECECE',
|
||||
'greyscale-350': '#ddd',
|
||||
'greyscale-400': '#929292',
|
||||
'greyscale-500': '#7C7C7C',
|
||||
'greyscale-600': '#666666',
|
||||
'greyscale-700': '#3A3A3A',
|
||||
'greyscale-750': '#353535',
|
||||
'greyscale-800': '#2A2A2A',
|
||||
'greyscale-900': '#242424',
|
||||
'greyscale-950': '#1E1E1E',
|
||||
'greyscale-1000': '#161616',
|
||||
'success-text': '#1f8d49',
|
||||
'success-100': '#dffee6',
|
||||
'success-200': '#b8fec9',
|
||||
'success-300': '#88fdaa',
|
||||
'success-400': '#3bea7e',
|
||||
'success-500': '#1f8d49',
|
||||
'success-600': '#18753c',
|
||||
'success-700': '#204129',
|
||||
'success-800': '#1e2e22',
|
||||
'success-900': '#19281d',
|
||||
'info-text': '#0078f3',
|
||||
'info-100': '#E8EDFF',
|
||||
'info-200': '#DDE5FF',
|
||||
'info-300': '#BCCDFF',
|
||||
'info-400': '#518FFF',
|
||||
'info-500': '#0078F3',
|
||||
'info-600': '#0063CB',
|
||||
'info-700': '#273961',
|
||||
'info-800': '#222A3F',
|
||||
'info-900': '#1D2437',
|
||||
'warning-text': '#d64d00',
|
||||
'warning-100': '#fff4f3',
|
||||
'warning-200': '#ffe9e6',
|
||||
'warning-300': '#ffded9',
|
||||
'warning-400': '#ffbeb4',
|
||||
'warning-500': '#d64d00',
|
||||
'warning-600': '#b34000',
|
||||
'warning-700': '#5e2c21',
|
||||
'warning-800': '#3e241e',
|
||||
'warning-900': '#361e19',
|
||||
'danger-text': '#FFF',
|
||||
'danger-100': '#FFE9E9',
|
||||
'danger-200': '#FFDDDD',
|
||||
'danger-300': '#FFBDBD',
|
||||
'danger-400': '#FF5655',
|
||||
'danger-500': '#F60700',
|
||||
'danger-600': '#CE0500',
|
||||
'danger-700': '#642626',
|
||||
'danger-800': '#412121',
|
||||
'danger-900': '#391C1C',
|
||||
},
|
||||
font: { families: { accent: 'Marianne', base: 'Marianne' } },
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
alert: {
|
||||
'border-radius': '0',
|
||||
error: {
|
||||
'background-color': 'var(--c--theme--colors--danger-100)',
|
||||
'border-left-color': 'var(--c--theme--colors--danger-400)',
|
||||
close: {
|
||||
color: 'white',
|
||||
'background-color': 'var(--c--theme--colors--danger-400)',
|
||||
'background-color-hover': 'var(--c--theme--colors--danger-600)',
|
||||
},
|
||||
},
|
||||
},
|
||||
modal: { 'width-small': '342px' },
|
||||
button: {
|
||||
'medium-height': '40px',
|
||||
'medium-text-height': '40px',
|
||||
'border-radius': '4px',
|
||||
primary: {
|
||||
background: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'color-hover': '#1212ff',
|
||||
'color-active': '#2323ff',
|
||||
},
|
||||
color: '#fff',
|
||||
'color-hover': '#fff',
|
||||
'color-active': '#fff',
|
||||
},
|
||||
'primary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
color: 'var(--c--theme--colors--primary-800)',
|
||||
},
|
||||
secondary: {
|
||||
background: { 'color-hover': '#F6F6F6', 'color-active': '#EDEDED' },
|
||||
border: {
|
||||
color: 'var(--c--theme--colors--greyscale-300)',
|
||||
'color-hover': 'var(--c--theme--colors--greyscale-300)',
|
||||
},
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
'tertiary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--greyscale-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
color: 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
},
|
||||
datagrid: {
|
||||
header: {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
size: 'var(--c--theme--font--sizes--s)',
|
||||
},
|
||||
body: {
|
||||
'background-color': 'transparent',
|
||||
'background-color-hover': '#F4F4FD',
|
||||
},
|
||||
pagination: {
|
||||
'background-color': 'transparent',
|
||||
'background-color-active': 'var(--c--theme--colors--primary-300)',
|
||||
'border-color': 'var(--c--theme--colors--primary-400)',
|
||||
},
|
||||
},
|
||||
'forms-checkbox': {
|
||||
'border-radius': '0',
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
text: {
|
||||
color: 'var(--c--theme--colors--greyscale-text)',
|
||||
size: 'var(--c--theme--font--sizes--t)',
|
||||
},
|
||||
},
|
||||
'forms-datepicker': { 'border-radius': '0' },
|
||||
'forms-fileuploader': { 'border-radius': '0' },
|
||||
'forms-field': {
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
'footer-font-size': 'var(--c--theme--font--sizes--t)',
|
||||
'footer-color': 'var(--c--theme--colors--greyscale-text)',
|
||||
},
|
||||
'forms-input': {
|
||||
'border-radius': '4px',
|
||||
'background-color': '#fff',
|
||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||
'value-color': 'var(--c--theme--colors--primary-text)',
|
||||
'font-size': '14px',
|
||||
},
|
||||
'forms-labelledbox': {
|
||||
'label-color': { big: 'var(--c--theme--colors--primary-text)' },
|
||||
},
|
||||
'forms-radio': {
|
||||
'accent-color': 'var(--c--theme--colors--primary-600)',
|
||||
},
|
||||
'forms-select': {
|
||||
'item-font-size': '14px',
|
||||
'border-radius': '4px',
|
||||
'border-radius-hover': '4px',
|
||||
'background-color': '#fff',
|
||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||
'border-color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
'forms-switch': {
|
||||
'handle-border-radius': '2px',
|
||||
'rail-border-radius': '4px',
|
||||
'accent-color': 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
'forms-textarea': { 'border-radius': '0' },
|
||||
'la-gauffre': { activated: true },
|
||||
'home-proconnect': { activated: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { create } from 'zustand';
|
||||
|
||||
import { tokens } from './cunningham-tokens';
|
||||
|
||||
type Tokens = typeof tokens.themes.default & Partial<typeof tokens.themes.dsfr>;
|
||||
type Tokens = typeof tokens.themes.default;
|
||||
type ColorsTokens = Tokens['theme']['colors'];
|
||||
type FontSizesTokens = Tokens['theme']['font']['sizes'];
|
||||
type SpacingsTokens = Tokens['theme']['spacings'];
|
||||
@@ -28,7 +28,7 @@ export const useCunninghamTheme = create<AuthStore>((set, get) => {
|
||||
) as Tokens;
|
||||
|
||||
return {
|
||||
theme: 'dsfr',
|
||||
theme: 'default',
|
||||
themeTokens: () => currentTheme().theme,
|
||||
colorsTokens: () => currentTheme().theme.colors,
|
||||
componentTokens: () => currentTheme().components,
|
||||
|
||||
@@ -161,6 +161,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
</Text>
|
||||
<Select
|
||||
clearable={false}
|
||||
fullWidth
|
||||
label={t('Template')}
|
||||
options={templateOptions}
|
||||
value={templateSelected}
|
||||
|
||||
@@ -21,9 +21,11 @@ describe('useSWRegister', () => {
|
||||
reject('error');
|
||||
}),
|
||||
);
|
||||
|
||||
Object.defineProperty(navigator, 'serviceWorker', {
|
||||
value: {
|
||||
register: registerSpy,
|
||||
addEventListener: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
@@ -31,6 +33,10 @@ describe('useSWRegister', () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(registerSpy).toHaveBeenCalledWith('/service-worker.js?v=123456');
|
||||
expect(navigator.serviceWorker.addEventListener).toHaveBeenCalledWith(
|
||||
'controllerchange',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('checks service-worker is not register', () => {
|
||||
|
||||
@@ -8,9 +8,33 @@ export const useSWRegister = () => {
|
||||
) {
|
||||
navigator.serviceWorker
|
||||
.register(`/service-worker.js?v=${process.env.NEXT_PUBLIC_BUILD_ID}`)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const newWorker = registration.installing;
|
||||
if (!newWorker) {
|
||||
return;
|
||||
}
|
||||
|
||||
newWorker.onstatechange = () => {
|
||||
if (
|
||||
newWorker.state === 'installed' &&
|
||||
navigator.serviceWorker.controller
|
||||
) {
|
||||
newWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Service worker registration failed:', err);
|
||||
});
|
||||
|
||||
const currentController = navigator.serviceWorker.controller;
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (currentController) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -65,6 +65,13 @@ self.addEventListener('install', function (event) {
|
||||
event.waitUntil(self.skipWaiting());
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (event.data?.type === 'SKIP_WAITING') {
|
||||
void self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
const cacheAllow = SW_VERSION;
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
@import url('../cunningham/cunningham-style.css');
|
||||
@import url('@gouvfr-lasuite/ui-kit/style');
|
||||
@import url('../cunningham/cunningham-tokens.css');
|
||||
@import url('../cunningham/cunningham-custom-tokens.css');
|
||||
@import url('@fontsource/material-icons');
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -41,3 +44,37 @@ main ::-webkit-scrollbar-thumb:hover,
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
.material-icons-filled {
|
||||
font-family: 'Material Icons', sans-serif;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px; /* Preferred icon size */
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizelegibility;
|
||||
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
}
|
||||
|
||||
[data-nextjs-dialog-overlay] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
nextjs-portal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const AppWrapper = ({ children }: PropsWithChildren) => {
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CunninghamProvider theme="dsfr">{children}</CunninghamProvider>
|
||||
<CunninghamProvider theme="default">{children}</CunninghamProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress",
|
||||
"version": "2.6.0",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-config-impress",
|
||||
"version": "2.6.0",
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "2.6.0",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:impress",
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
HocuspocusProvider,
|
||||
HocuspocusProviderWebsocket,
|
||||
} from '@hocuspocus/provider';
|
||||
import { v1 as uuidv1, v4 as uuidv4 } from 'uuid';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
const port = 5559;
|
||||
@@ -13,11 +14,22 @@ jest.mock('../src/env', () => {
|
||||
PORT: port,
|
||||
COLLABORATION_SERVER_ORIGIN: origin,
|
||||
COLLABORATION_SERVER_SECRET: 'test-secret-api-key',
|
||||
COLLABORATION_BACKEND_BASE_URL: 'http://app-dev:8000',
|
||||
};
|
||||
});
|
||||
|
||||
console.error = jest.fn();
|
||||
|
||||
const mockDocFetch = jest.fn();
|
||||
jest.mock('@/api/getDoc', () => ({
|
||||
fetchDocument: mockDocFetch,
|
||||
}));
|
||||
|
||||
const mockGetMe = jest.fn();
|
||||
jest.mock('@/api/getMe', () => ({
|
||||
getMe: mockGetMe,
|
||||
}));
|
||||
|
||||
import { hocusPocusServer } from '@/servers/hocusPocusServer';
|
||||
|
||||
import { promiseDone } from '../src/helpers';
|
||||
@@ -30,45 +42,20 @@ describe('Server Tests', () => {
|
||||
await hocusPocusServer.configure({ port: portWS }).listen();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
void hocusPocusServer.destroy();
|
||||
});
|
||||
|
||||
test('WebSocket connection with correct API key can connect', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
// eslint-disable-next-line jest/unbound-method
|
||||
const { handleConnection } = hocusPocusServer;
|
||||
const mockHandleConnection = jest.fn();
|
||||
(hocusPocusServer.handleConnection as jest.Mock) = mockHandleConnection;
|
||||
|
||||
const clientWS = new WebSocket(
|
||||
`ws://localhost:${port}/collaboration/ws/?room=test-room`,
|
||||
{
|
||||
headers: {
|
||||
authorization: 'test-secret-api-key',
|
||||
Origin: origin,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
clientWS.on('open', () => {
|
||||
expect(mockHandleConnection).toHaveBeenCalled();
|
||||
clientWS.close();
|
||||
mockHandleConnection.mockClear();
|
||||
hocusPocusServer.handleConnection = handleConnection;
|
||||
done();
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('WebSocket connection with bad origin should be closed', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
const room = uuidv4();
|
||||
const ws = new WebSocket(
|
||||
`ws://localhost:${port}/collaboration/ws/?room=test-room`,
|
||||
`ws://localhost:${port}/collaboration/ws/?room=${room}`,
|
||||
{
|
||||
headers: {
|
||||
Origin: 'http://bad-origin.com',
|
||||
@@ -84,13 +71,13 @@ describe('Server Tests', () => {
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('WebSocket connection with incorrect API key should be closed', () => {
|
||||
test('WebSocket connection without cookies header should be closed', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
const room = uuidv4();
|
||||
const ws = new WebSocket(
|
||||
`ws://localhost:${port}/collaboration/ws/?room=test-room`,
|
||||
`ws://localhost:${port}/collaboration/ws/?room=${room}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'wrong-api-key',
|
||||
Origin: origin,
|
||||
},
|
||||
},
|
||||
@@ -106,21 +93,28 @@ describe('Server Tests', () => {
|
||||
|
||||
test('WebSocket connection not allowed if room not matching provider name', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
const room = uuidv4();
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=my-test`,
|
||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
maxAttempts: 1,
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
const providerName = uuidv4();
|
||||
const provider = new HocuspocusProvider({
|
||||
websocketProvider: wsHocus,
|
||||
name: 'hocuspocus-test',
|
||||
name: providerName,
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
preserveConnection: false,
|
||||
onClose: (data) => {
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Invalid room name - Probable hacking attempt:',
|
||||
providerName,
|
||||
room,
|
||||
);
|
||||
|
||||
wsHocus.stopConnectionAttempt();
|
||||
expect(data.event.reason).toBe('Forbidden');
|
||||
wsHocus.webSocket?.close();
|
||||
@@ -134,30 +128,32 @@ describe('Server Tests', () => {
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('WebSocket connection read-only', () => {
|
||||
test('WebSocket connection not allowed if room is not a valid uuid v4', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
const room = uuidv1();
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=hocuspocus-test`,
|
||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
maxAttempts: 1,
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
websocketProvider: wsHocus,
|
||||
name: 'hocuspocus-test',
|
||||
name: room,
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
onConnect: () => {
|
||||
void hocusPocusServer
|
||||
.openDirectConnection('hocuspocus-test')
|
||||
.then((connection) => {
|
||||
connection.document?.getConnections().forEach((connection) => {
|
||||
expect(connection.readOnly).toBe(true);
|
||||
});
|
||||
|
||||
void connection.disconnect();
|
||||
});
|
||||
preserveConnection: false,
|
||||
onClose: (data) => {
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Room name is not a valid uuid:',
|
||||
room,
|
||||
);
|
||||
|
||||
wsHocus.stopConnectionAttempt();
|
||||
expect(data.event.reason).toBe('Forbidden');
|
||||
wsHocus.webSocket?.close();
|
||||
wsHocus.disconnect();
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
@@ -166,4 +162,206 @@ describe('Server Tests', () => {
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('WebSocket connection not allowed if room is not a valid uuid', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
const room = 'not-a-valid-uuid';
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
maxAttempts: 1,
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
websocketProvider: wsHocus,
|
||||
name: room,
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
preserveConnection: false,
|
||||
onClose: (data) => {
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Room name is not a valid uuid:',
|
||||
room,
|
||||
);
|
||||
|
||||
wsHocus.stopConnectionAttempt();
|
||||
expect(data.event.reason).toBe('Forbidden');
|
||||
wsHocus.webSocket?.close();
|
||||
wsHocus.disconnect();
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
},
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('WebSocket connection fails if user can not access document', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
mockDocFetch.mockRejectedValue('');
|
||||
|
||||
const room = uuidv4();
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
maxAttempts: 1,
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
websocketProvider: wsHocus,
|
||||
name: room,
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
preserveConnection: false,
|
||||
onClose: (data) => {
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'[onConnect]',
|
||||
'Backend error: Unauthorized',
|
||||
);
|
||||
|
||||
wsHocus.stopConnectionAttempt();
|
||||
expect(data.event.reason).toBe('Forbidden');
|
||||
expect(mockDocFetch).toHaveBeenCalledTimes(1);
|
||||
wsHocus.webSocket?.close();
|
||||
wsHocus.disconnect();
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
},
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('WebSocket connection fails if user do not have correct retrieve ability', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
const room = uuidv4();
|
||||
mockDocFetch.mockResolvedValue({
|
||||
abilities: {
|
||||
retrieve: false,
|
||||
},
|
||||
});
|
||||
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
maxAttempts: 1,
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
websocketProvider: wsHocus,
|
||||
name: room,
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
preserveConnection: false,
|
||||
onClose: (data) => {
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'onConnect: Unauthorized to retrieve this document',
|
||||
room,
|
||||
);
|
||||
|
||||
wsHocus.stopConnectionAttempt();
|
||||
expect(data.event.reason).toBe('Forbidden');
|
||||
expect(mockDocFetch).toHaveBeenCalledTimes(1);
|
||||
wsHocus.webSocket?.close();
|
||||
wsHocus.disconnect();
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
},
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
[true, false].forEach((canEdit) => {
|
||||
test(`WebSocket connection ${canEdit ? 'can' : 'can not'} edit document`, () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
mockDocFetch.mockResolvedValue({
|
||||
abilities: {
|
||||
retrieve: true,
|
||||
update: canEdit,
|
||||
},
|
||||
});
|
||||
|
||||
const room = uuidv4();
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
});
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
websocketProvider: wsHocus,
|
||||
name: room,
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
onConnect: () => {
|
||||
void hocusPocusServer
|
||||
.openDirectConnection(room)
|
||||
.then((connection) => {
|
||||
connection.document?.getConnections().forEach((connection) => {
|
||||
expect(connection.readOnly).toBe(!canEdit);
|
||||
});
|
||||
|
||||
void connection.disconnect();
|
||||
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
});
|
||||
|
||||
test('Add request header x-user-id if found', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
mockDocFetch.mockResolvedValue({
|
||||
abilities: {
|
||||
retrieve: true,
|
||||
update: true,
|
||||
},
|
||||
});
|
||||
|
||||
mockGetMe.mockResolvedValue({
|
||||
id: 'test-user-id',
|
||||
});
|
||||
|
||||
const room = uuidv4();
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
});
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
websocketProvider: wsHocus,
|
||||
name: room,
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
onConnect: () => {
|
||||
void hocusPocusServer.openDirectConnection(room).then((connection) => {
|
||||
connection.document?.getConnections().forEach((connection) => {
|
||||
expect(connection.context.userId).toBe('test-user-id');
|
||||
});
|
||||
|
||||
void connection.disconnect();
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server-y-provider",
|
||||
"version": "2.6.0",
|
||||
"version": "3.0.0",
|
||||
"description": "Y.js provider for docs",
|
||||
"repository": "https://github.com/numerique-gouv/impress",
|
||||
"license": "MIT",
|
||||
@@ -20,9 +20,11 @@
|
||||
"@hocuspocus/server": "2.15.2",
|
||||
"@sentry/node": "9.3.0",
|
||||
"@sentry/profiling-node": "9.3.0",
|
||||
"axios": "1.8.2",
|
||||
"cors": "2.8.5",
|
||||
"express": "4.21.2",
|
||||
"express-ws": "5.0.2",
|
||||
"uuid": "11.1.0",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "*"
|
||||
},
|
||||
|
||||
76
src/frontend/servers/y-provider/src/api/getDoc.ts
Normal file
76
src/frontend/servers/y-provider/src/api/getDoc.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { COLLABORATION_BACKEND_BASE_URL } from '@/env';
|
||||
|
||||
enum LinkReach {
|
||||
RESTRICTED = 'restricted',
|
||||
PUBLIC = 'public',
|
||||
AUTHENTICATED = 'authenticated',
|
||||
}
|
||||
|
||||
enum LinkRole {
|
||||
READER = 'reader',
|
||||
EDITOR = 'editor',
|
||||
}
|
||||
|
||||
type Base64 = string;
|
||||
|
||||
interface Doc {
|
||||
id: string;
|
||||
title?: string;
|
||||
content: Base64;
|
||||
creator: string;
|
||||
is_favorite: boolean;
|
||||
link_reach: LinkReach;
|
||||
link_role: LinkRole;
|
||||
nb_accesses_ancestors: number;
|
||||
nb_accesses_direct: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
abilities: {
|
||||
accesses_manage: boolean;
|
||||
accesses_view: boolean;
|
||||
ai_transform: boolean;
|
||||
ai_translate: boolean;
|
||||
attachment_upload: boolean;
|
||||
children_create: boolean;
|
||||
children_list: boolean;
|
||||
collaboration_auth: boolean;
|
||||
destroy: boolean;
|
||||
favorite: boolean;
|
||||
invite_owner: boolean;
|
||||
link_configuration: boolean;
|
||||
media_auth: boolean;
|
||||
move: boolean;
|
||||
partial_update: boolean;
|
||||
restore: boolean;
|
||||
retrieve: boolean;
|
||||
update: boolean;
|
||||
versions_destroy: boolean;
|
||||
versions_list: boolean;
|
||||
versions_retrieve: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchDocument = async (
|
||||
documentName: string,
|
||||
requestHeaders: IncomingHttpHeaders,
|
||||
) => {
|
||||
const response = await axios.get<Doc>(
|
||||
`${COLLABORATION_BACKEND_BASE_URL}/api/v1.0/documents/${documentName}/`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: requestHeaders['cookie'],
|
||||
Origin: requestHeaders['origin'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch document: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
31
src/frontend/servers/y-provider/src/api/getMe.ts
Normal file
31
src/frontend/servers/y-provider/src/api/getMe.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { COLLABORATION_BACKEND_BASE_URL } from '@/env';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
short_name: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
export const getMe = async (requestHeaders: IncomingHttpHeaders) => {
|
||||
const response = await axios.get<User>(
|
||||
`${COLLABORATION_BACKEND_BASE_URL}/api/v1.0/users/me/`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: requestHeaders['cookie'],
|
||||
Origin: requestHeaders['origin'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch user: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
@@ -8,3 +8,5 @@ export const Y_PROVIDER_API_KEY =
|
||||
process.env.Y_PROVIDER_API_KEY || 'yprovider-api-key';
|
||||
export const PORT = Number(process.env.PORT || 4444);
|
||||
export const SENTRY_DSN = process.env.SENTRY_DSN || '';
|
||||
export const COLLABORATION_BACKEND_BASE_URL =
|
||||
process.env.COLLABORATION_BACKEND_BASE_URL || 'http://app-dev:8000';
|
||||
|
||||
@@ -14,14 +14,7 @@ export const collaborationResetConnectionsHandler = (
|
||||
const room = req.query.room;
|
||||
const userId = req.headers['x-user-id'];
|
||||
|
||||
logger(
|
||||
'Resetting connections in room:',
|
||||
room,
|
||||
'for user:',
|
||||
userId,
|
||||
'room:',
|
||||
room,
|
||||
);
|
||||
logger('Resetting connections in room:', room, 'for user:', userId);
|
||||
|
||||
if (!room) {
|
||||
res.status(400).json({ error: 'Room name not provided' });
|
||||
@@ -43,8 +36,8 @@ export const collaborationResetConnectionsHandler = (
|
||||
}
|
||||
|
||||
doc.getConnections().forEach((connection) => {
|
||||
const connectionUserId = connection.request.headers['x-user-id'];
|
||||
if (connectionUserId === userId) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (connection.context.userId === userId) {
|
||||
connection.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,17 +40,16 @@ export const wsSecurity = (
|
||||
): void => {
|
||||
// Origin check
|
||||
const origin = req.headers['origin'];
|
||||
if (origin && !allowedOrigins.includes(origin)) {
|
||||
if (!origin || !allowedOrigins.includes(origin)) {
|
||||
ws.close(4001, 'Origin not allowed');
|
||||
console.error('CORS policy violation: Invalid Origin', origin);
|
||||
return;
|
||||
}
|
||||
|
||||
// Secret API Key check
|
||||
const apiKey = req.headers['authorization'];
|
||||
if (apiKey !== COLLABORATION_SERVER_SECRET) {
|
||||
console.error('Forbidden: Invalid API Key');
|
||||
ws.close();
|
||||
const cookies = req.headers['cookie'];
|
||||
if (!cookies) {
|
||||
ws.close(4001, 'No cookies');
|
||||
console.error('CORS policy violation: No cookies');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +1,81 @@
|
||||
import { Server } from '@hocuspocus/server';
|
||||
import { validate as uuidValidate, version as uuidVersion } from 'uuid';
|
||||
|
||||
import { fetchDocument } from '@/api/getDoc';
|
||||
import { getMe } from '@/api/getMe';
|
||||
import { logger } from '@/utils';
|
||||
|
||||
export const hocusPocusServer = Server.configure({
|
||||
name: 'docs-collaboration',
|
||||
timeout: 30000,
|
||||
quiet: true,
|
||||
onConnect({ requestHeaders, connection, documentName, requestParameters }) {
|
||||
async onConnect({
|
||||
requestHeaders,
|
||||
connection,
|
||||
documentName,
|
||||
requestParameters,
|
||||
context,
|
||||
}) {
|
||||
const roomParam = requestParameters.get('room');
|
||||
const canEdit = requestHeaders['x-can-edit'] === 'True';
|
||||
|
||||
if (!canEdit) {
|
||||
connection.readOnly = true;
|
||||
}
|
||||
|
||||
logger(
|
||||
'Connection established:',
|
||||
documentName,
|
||||
'userId:',
|
||||
requestHeaders['x-user-id'],
|
||||
'canEdit:',
|
||||
canEdit,
|
||||
'room:',
|
||||
requestParameters.get('room'),
|
||||
);
|
||||
|
||||
if (documentName !== roomParam) {
|
||||
console.error(
|
||||
'Invalid room name - Probable hacking attempt:',
|
||||
documentName,
|
||||
requestParameters.get('room'),
|
||||
requestHeaders['x-user-id'],
|
||||
);
|
||||
|
||||
return Promise.reject(new Error('Unauthorized'));
|
||||
return Promise.reject(new Error('Wrong room name: Unauthorized'));
|
||||
}
|
||||
|
||||
if (!uuidValidate(documentName) || uuidVersion(documentName) !== 4) {
|
||||
console.error('Room name is not a valid uuid:', documentName);
|
||||
|
||||
return Promise.reject(new Error('Wrong room name: Unauthorized'));
|
||||
}
|
||||
|
||||
let can_edit = false;
|
||||
|
||||
try {
|
||||
const document = await fetchDocument(documentName, requestHeaders);
|
||||
|
||||
if (!document.abilities.retrieve) {
|
||||
console.error(
|
||||
'onConnect: Unauthorized to retrieve this document',
|
||||
documentName,
|
||||
);
|
||||
return Promise.reject(new Error('Wrong abilities:Unauthorized'));
|
||||
}
|
||||
|
||||
can_edit = document.abilities.update;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error('onConnect: backend error', error.message);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('Backend error: Unauthorized'));
|
||||
}
|
||||
|
||||
connection.readOnly = !can_edit;
|
||||
|
||||
/*
|
||||
* Unauthenticated users can be allowed to connect
|
||||
* so we flag only authenticated users
|
||||
*/
|
||||
try {
|
||||
const user = await getMe(requestHeaders);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
context.userId = user.id;
|
||||
} catch {}
|
||||
|
||||
logger(
|
||||
'Connection established:',
|
||||
documentName,
|
||||
'canEdit:',
|
||||
can_edit,
|
||||
'room:',
|
||||
requestParameters.get('room'),
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -225,12 +225,12 @@
|
||||
"@babel/types" "^7.25.9"
|
||||
|
||||
"@babel/helpers@^7.26.7":
|
||||
version "7.26.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384"
|
||||
integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==
|
||||
version "7.26.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.7.tgz#fd1d2a7c431b6e39290277aacfd8367857c576a4"
|
||||
integrity sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==
|
||||
dependencies:
|
||||
"@babel/template" "^7.26.9"
|
||||
"@babel/types" "^7.26.10"
|
||||
"@babel/template" "^7.25.9"
|
||||
"@babel/types" "^7.26.7"
|
||||
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.25.9", "@babel/parser@^7.26.5", "@babel/parser@^7.26.7":
|
||||
version "7.26.7"
|
||||
@@ -239,13 +239,6 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.26.7"
|
||||
|
||||
"@babel/parser@^7.26.9":
|
||||
version "7.26.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.10.tgz#e9bdb82f14b97df6569b0b038edd436839c57749"
|
||||
integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.26.10"
|
||||
|
||||
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe"
|
||||
@@ -970,6 +963,13 @@
|
||||
"@babel/plugin-transform-typescript" "^7.25.9"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.5", "@babel/runtime@^7.25.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
||||
version "7.26.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.7.tgz#f4e7fe527cd710f8dc0618610b61b4b060c3c341"
|
||||
integrity sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.9.2":
|
||||
version "7.26.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2"
|
||||
integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==
|
||||
@@ -985,15 +985,6 @@
|
||||
"@babel/parser" "^7.25.9"
|
||||
"@babel/types" "^7.25.9"
|
||||
|
||||
"@babel/template@^7.26.9":
|
||||
version "7.26.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2"
|
||||
integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.26.2"
|
||||
"@babel/parser" "^7.26.9"
|
||||
"@babel/types" "^7.26.9"
|
||||
|
||||
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.7":
|
||||
version "7.26.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.7.tgz#99a0a136f6a75e7fb8b0a1ace421e0b25994b8bb"
|
||||
@@ -1015,14 +1006,6 @@
|
||||
"@babel/helper-string-parser" "^7.25.9"
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
|
||||
"@babel/types@^7.26.10", "@babel/types@^7.26.9":
|
||||
version "7.26.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.10.tgz#396382f6335bd4feb65741eacfc808218f859259"
|
||||
integrity sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.25.9"
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
|
||||
"@bcoe/v8-coverage@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
@@ -1242,6 +1225,45 @@
|
||||
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz#037817b574262134cabd68fc4ec1a454f168407b"
|
||||
integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==
|
||||
|
||||
"@dnd-kit/accessibility@^3.1.1":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz#3b4202bd6bb370a0730f6734867785919beac6af"
|
||||
integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/core@6.3.1":
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.3.1.tgz#4c36406a62c7baac499726f899935f93f0e6d003"
|
||||
integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==
|
||||
dependencies:
|
||||
"@dnd-kit/accessibility" "^3.1.1"
|
||||
"@dnd-kit/utilities" "^3.2.2"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/modifiers@9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz#96a0280c77b10c716ef79d9792ce7ad04370771d"
|
||||
integrity sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==
|
||||
dependencies:
|
||||
"@dnd-kit/utilities" "^3.2.2"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/sortable@10.0.0":
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz#1f9382b90d835cd5c65d92824fa9dafb78c4c3e8"
|
||||
integrity sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==
|
||||
dependencies:
|
||||
"@dnd-kit/utilities" "^3.2.2"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/utilities@^3.2.2":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b"
|
||||
integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dual-bundle/import-meta-resolve@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b"
|
||||
@@ -1567,6 +1589,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/material-icons-outlined/-/material-icons-outlined-5.1.1.tgz#3a659277f5029fb1ea5f64739d7ef48fd2112a8e"
|
||||
integrity sha512-HjWe3anHu9RptoAvm2UF6MPXkwsbFbH+vkg48GRIhXPsXyfmZteGHVFZpPxaZnakrmnLXDxUnnKSj7zzqk+Rhg==
|
||||
|
||||
"@fontsource/material-icons@5.2.5":
|
||||
version "5.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/material-icons/-/material-icons-5.2.5.tgz#cdb9dd23c0da4b021c866dc529ed4c203414ec3c"
|
||||
integrity sha512-9k0LBRVgResIeD+vC/epYmm/awN2k2L8twwEtUWQ3FHluMi+7PbISOpXqksvfqPn9FJy4/KEeWOhFTR/SrbhNw==
|
||||
|
||||
"@formatjs/ecma402-abstract@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz#0ee291effe7ee2c340742a6c95d92eacb5e6c00a"
|
||||
@@ -1613,6 +1640,25 @@
|
||||
resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/integration/-/integration-1.0.2.tgz#ed0000f4b738c5a19bb60f5b80a9a2f5d9414234"
|
||||
integrity sha512-npOotZQSyu6SffHiPP+jQVOkJ3qW2KE2cANhEK92sNLX9uZqQaCqljO5GhzsBmh0lB76fiXnrr9i8SIpnDUSZg==
|
||||
|
||||
"@gouvfr-lasuite/ui-kit@0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/ui-kit/-/ui-kit-0.1.3.tgz#1be7f1bdf12e7428e630d6ce11fbc77c4c9b9b21"
|
||||
integrity sha512-ba3ZrAIhX84cofa2IwiWhgE0wzz85+ySbOTvB1lP9jeWYvWn/N5HsnxphA9bEMIrx1Yi91upzmYLvjHRoDq1Ww==
|
||||
dependencies:
|
||||
"@dnd-kit/core" "6.3.1"
|
||||
"@dnd-kit/modifiers" "9.0.0"
|
||||
"@dnd-kit/sortable" "10.0.0"
|
||||
"@openfun/cunningham-react" "3.0.0"
|
||||
"@types/node" "22.10.7"
|
||||
clsx "2.1.1"
|
||||
cmdk "1.0.4"
|
||||
react "19.0.0"
|
||||
react-arborist "3.4.3"
|
||||
react-aria-components "1.6.0"
|
||||
react-dom "19.0.0"
|
||||
react-resizable-panels "2.1.7"
|
||||
react-stately "3.35.0"
|
||||
|
||||
"@gulpjs/to-absolute-glob@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz#1fc2460d3953e1d9b9f2dfdb4bcc99da4710c021"
|
||||
@@ -2107,10 +2153,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@mantine/utils/-/utils-6.0.22.tgz#7eace697084e2bc5a831eb0fd7cbbc04cc1b0354"
|
||||
integrity sha512-RSKlNZvxhMCkOFZ6slbYvZYbWjHUM+PxDQnupIOxIdsTZQQjx/BFfrfJ7kQFOP+g7MtpOds8weAetEs5obwMOQ==
|
||||
|
||||
"@next/env@15.2.1":
|
||||
version "15.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.1.tgz#4b5baade67031fb4cc3c1841c661dde3db24bde4"
|
||||
integrity sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q==
|
||||
"@next/env@15.2.3":
|
||||
version "15.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.3.tgz#037ee37c4d61fcbdbb212694cc33d7dcf6c7975a"
|
||||
integrity sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==
|
||||
|
||||
"@next/eslint-plugin-next@15.2.1":
|
||||
version "15.2.1"
|
||||
@@ -2119,45 +2165,45 @@
|
||||
dependencies:
|
||||
fast-glob "3.3.1"
|
||||
|
||||
"@next/swc-darwin-arm64@15.2.1":
|
||||
version "15.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.1.tgz#bfca841bb7cfbb9f9e05f63ffe3e574ac6679308"
|
||||
integrity sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ==
|
||||
"@next/swc-darwin-arm64@15.2.3":
|
||||
version "15.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.3.tgz#2688c185651ef7a16e5642c85048cc4e151159fa"
|
||||
integrity sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw==
|
||||
|
||||
"@next/swc-darwin-x64@15.2.1":
|
||||
version "15.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.1.tgz#4949a51c74e3ed657c4fda17e1f408f8cb5cdda4"
|
||||
integrity sha512-E/w8ervu4fcG5SkLhvn1NE/2POuDCDEy5gFbfhmnYXkyONZR68qbUlJlZwuN82o7BrBVAw+tkR8nTIjGiMW1jQ==
|
||||
"@next/swc-darwin-x64@15.2.3":
|
||||
version "15.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.3.tgz#3e802259b2c9a4e2ad55ff827f41f775b726fc7d"
|
||||
integrity sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@15.2.1":
|
||||
version "15.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.1.tgz#449b08825e2e034408878ca9b0974786cc98cc45"
|
||||
integrity sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ==
|
||||
"@next/swc-linux-arm64-gnu@15.2.3":
|
||||
version "15.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.3.tgz#315d7b54b89153f125bdc3e40bcb7ccf94ef124b"
|
||||
integrity sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ==
|
||||
|
||||
"@next/swc-linux-arm64-musl@15.2.1":
|
||||
version "15.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.1.tgz#4f4d325c3ede762cdf5c2b34bfd1d5fa7d95a0d1"
|
||||
integrity sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg==
|
||||
"@next/swc-linux-arm64-musl@15.2.3":
|
||||
version "15.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.3.tgz#a1a458eb7cf19c59d2014ee388a7305e9a77973f"
|
||||
integrity sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==
|
||||
|
||||
"@next/swc-linux-x64-gnu@15.2.1":
|
||||
version "15.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.1.tgz#9ab4f4d5f3d9e28d56fa0e0933aef3282caff6c8"
|
||||
integrity sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww==
|
||||
"@next/swc-linux-x64-gnu@15.2.3":
|
||||
version "15.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.3.tgz#a3cf22eda7601536ccd68e8ba4c1bfb4a1a33460"
|
||||
integrity sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==
|
||||
|
||||
"@next/swc-linux-x64-musl@15.2.1":
|
||||
version "15.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.1.tgz#51ed130f5b44150d85197a90c471f19339ac2e67"
|
||||
integrity sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg==
|
||||
"@next/swc-linux-x64-musl@15.2.3":
|
||||
version "15.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.3.tgz#0e33c1224c76aa3078cc2249c80ef583f9d7a943"
|
||||
integrity sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@15.2.1":
|
||||
version "15.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.1.tgz#180c9c1dde48df749768b19af35f1118aa334bd8"
|
||||
integrity sha512-Gk42XZXo1cE89i3hPLa/9KZ8OuupTjkDmhLaMKFohjf9brOeZVEa3BQy1J9s9TWUqPhgAEbwv6B2+ciGfe54Vw==
|
||||
"@next/swc-win32-arm64-msvc@15.2.3":
|
||||
version "15.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.3.tgz#4e0583fb981b931915a9ad22e579f9c9d5b803dd"
|
||||
integrity sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==
|
||||
|
||||
"@next/swc-win32-x64-msvc@15.2.1":
|
||||
version "15.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.1.tgz#0448f9fcddc035969b21c83da4c62d0fbbb5cec8"
|
||||
integrity sha512-YjqXCl8QGhVlMR8uBftWk0iTmvtntr41PhG1kvzGp0sUP/5ehTM+cwx25hKE54J0CRnHYjSGjSH3gkHEaHIN9g==
|
||||
"@next/swc-win32-x64-msvc@15.2.3":
|
||||
version "15.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.3.tgz#727b90c7dcc2279344115a94b99d93d452956f02"
|
||||
integrity sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -4049,6 +4095,21 @@
|
||||
"@react-types/shared" "^3.27.0"
|
||||
"@swc/helpers" "^0.5.0"
|
||||
|
||||
"@react-dnd/asap@^4.0.0":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
|
||||
integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==
|
||||
|
||||
"@react-dnd/invariant@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e"
|
||||
integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==
|
||||
|
||||
"@react-dnd/shallowequal@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
|
||||
integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
|
||||
|
||||
"@react-pdf/fns@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-pdf/fns/-/fns-3.0.0.tgz#2e0137d48b14c531b2f6a9214cb36ea2a7aea3ba"
|
||||
@@ -6273,13 +6334,20 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*", "@types/node@22.13.9", "@types/node@^22.7.5":
|
||||
"@types/node@*", "@types/node@^22.7.5":
|
||||
version "22.13.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.9.tgz#5d9a8f7a975a5bd3ef267352deb96fb13ec02eca"
|
||||
integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
"@types/node@22.10.7":
|
||||
version "22.10.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.7.tgz#14a1ca33fd0ebdd9d63593ed8d3fbc882a6d28d7"
|
||||
integrity sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239"
|
||||
@@ -6335,7 +6403,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
|
||||
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
||||
|
||||
"@types/react-dom@*", "@types/react-dom@19.0.0":
|
||||
"@types/react-dom@*":
|
||||
version "19.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.0.tgz#e7f5d618a080486eaf9952246dbf59eaa2c64130"
|
||||
integrity sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w==
|
||||
@@ -6354,7 +6422,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044"
|
||||
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
||||
|
||||
"@types/react@*", "@types/react@19.0.0":
|
||||
"@types/react@*":
|
||||
version "19.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.0.tgz#fbbb53ce223f4e2b750ad5dd09580b2c43522bbf"
|
||||
integrity sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==
|
||||
@@ -6472,7 +6540,7 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@8.26.0", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz#7e880faf91f89471c30c141951e15f0eb3a0599e"
|
||||
integrity sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==
|
||||
@@ -6487,7 +6555,7 @@
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/parser@*", "@typescript-eslint/parser@8.26.0", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
"@typescript-eslint/parser@*", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.0.tgz#9b4d2198e89f64fb81e83167eedd89a827d843a9"
|
||||
integrity sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==
|
||||
@@ -7046,6 +7114,15 @@ axe-core@^4.10.0:
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df"
|
||||
integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==
|
||||
|
||||
axios@1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
|
||||
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
axobject-query@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee"
|
||||
@@ -7547,7 +7624,7 @@ clone@^2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
|
||||
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
|
||||
|
||||
clsx@^2.0.0, clsx@^2.1.1:
|
||||
clsx@2.1.1, clsx@^2.0.0, clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
@@ -8124,6 +8201,15 @@ dir-glob@^3.0.1:
|
||||
dependencies:
|
||||
path-type "^4.0.0"
|
||||
|
||||
dnd-core@14.0.1:
|
||||
version "14.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e"
|
||||
integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==
|
||||
dependencies:
|
||||
"@react-dnd/asap" "^4.0.0"
|
||||
"@react-dnd/invariant" "^2.0.0"
|
||||
redux "^4.1.1"
|
||||
|
||||
doctrine@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
|
||||
@@ -8714,7 +8800,7 @@ eslint-visitor-keys@^4.2.0:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45"
|
||||
integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==
|
||||
|
||||
eslint@*, eslint@8.57.0:
|
||||
eslint@*:
|
||||
version "8.57.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668"
|
||||
integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==
|
||||
@@ -9110,6 +9196,11 @@ flatted@^3.3.3:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
|
||||
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
|
||||
|
||||
follow-redirects@^1.15.6:
|
||||
version "1.15.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
|
||||
fontkit@^2.0.2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/fontkit/-/fontkit-2.0.4.tgz#4765d664c68b49b5d6feb6bd1051ee49d8ec5ab0"
|
||||
@@ -11484,6 +11575,11 @@ media-typer@0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
||||
|
||||
"memoize-one@>=3.1.1 <6":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
||||
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||
|
||||
memoize-one@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
|
||||
@@ -11962,12 +12058,12 @@ neo-async@^2.6.2:
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||
|
||||
next@15.2.1:
|
||||
version "15.2.1"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.2.1.tgz#84276e5422ef0daa574a38f1a9db94084dc73b54"
|
||||
integrity sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g==
|
||||
next@15.2.3:
|
||||
version "15.2.3"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.2.3.tgz#1ac803c08076d47eb5b431cb625135616c6bec7e"
|
||||
integrity sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==
|
||||
dependencies:
|
||||
"@next/env" "15.2.1"
|
||||
"@next/env" "15.2.3"
|
||||
"@swc/counter" "0.1.3"
|
||||
"@swc/helpers" "0.5.15"
|
||||
busboy "1.6.0"
|
||||
@@ -11975,14 +12071,14 @@ next@15.2.1:
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.6"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "15.2.1"
|
||||
"@next/swc-darwin-x64" "15.2.1"
|
||||
"@next/swc-linux-arm64-gnu" "15.2.1"
|
||||
"@next/swc-linux-arm64-musl" "15.2.1"
|
||||
"@next/swc-linux-x64-gnu" "15.2.1"
|
||||
"@next/swc-linux-x64-musl" "15.2.1"
|
||||
"@next/swc-win32-arm64-msvc" "15.2.1"
|
||||
"@next/swc-win32-x64-msvc" "15.2.1"
|
||||
"@next/swc-darwin-arm64" "15.2.3"
|
||||
"@next/swc-darwin-x64" "15.2.3"
|
||||
"@next/swc-linux-arm64-gnu" "15.2.3"
|
||||
"@next/swc-linux-arm64-musl" "15.2.3"
|
||||
"@next/swc-linux-x64-gnu" "15.2.3"
|
||||
"@next/swc-linux-x64-musl" "15.2.3"
|
||||
"@next/swc-win32-arm64-msvc" "15.2.3"
|
||||
"@next/swc-win32-x64-msvc" "15.2.3"
|
||||
sharp "^0.33.5"
|
||||
|
||||
no-case@^3.0.4:
|
||||
@@ -12930,6 +13026,17 @@ raw-body@2.5.2:
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
react-arborist@3.4.3:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/react-arborist/-/react-arborist-3.4.3.tgz#982791a07d1e279f279be88162c920112f3cee90"
|
||||
integrity sha512-yFnq1nIQhT2uJY4TZVz2tgAiBb9lxSyvF4vC3S8POCK8xLzjGIxVv3/4dmYquQJ7AHxaZZArRGHiHKsEewKdTQ==
|
||||
dependencies:
|
||||
react-dnd "^14.0.3"
|
||||
react-dnd-html5-backend "^14.0.3"
|
||||
react-window "^1.8.11"
|
||||
redux "^5.0.0"
|
||||
use-sync-external-store "^1.2.0"
|
||||
|
||||
react-aria-components@1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-aria-components/-/react-aria-components-1.5.0.tgz#9c51ba8427e827d1192f965249e16ff7d34cbb55"
|
||||
@@ -13146,6 +13253,24 @@ react-aria@^3.37.0:
|
||||
"@react-aria/visually-hidden" "^3.8.19"
|
||||
"@react-types/shared" "^3.27.0"
|
||||
|
||||
react-dnd-html5-backend@^14.0.3:
|
||||
version "14.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f"
|
||||
integrity sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==
|
||||
dependencies:
|
||||
dnd-core "14.0.1"
|
||||
|
||||
react-dnd@^14.0.3:
|
||||
version "14.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.5.tgz#ecf264e220ae62e35634d9b941502f3fca0185ed"
|
||||
integrity sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==
|
||||
dependencies:
|
||||
"@react-dnd/invariant" "^2.0.0"
|
||||
"@react-dnd/shallowequal" "^2.0.0"
|
||||
dnd-core "14.0.1"
|
||||
fast-deep-equal "^3.1.3"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
|
||||
react-dom@*, react-dom@19.0.0:
|
||||
version "19.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57"
|
||||
@@ -13230,6 +13355,11 @@ react-remove-scroll@^2.6.2:
|
||||
use-callback-ref "^1.3.3"
|
||||
use-sidecar "^1.1.3"
|
||||
|
||||
react-resizable-panels@2.1.7:
|
||||
version "2.1.7"
|
||||
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz#afd29d8a3d708786a9f95183a38803c89f13c2e7"
|
||||
integrity sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==
|
||||
|
||||
react-select@5.10.1:
|
||||
version "5.10.1"
|
||||
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.10.1.tgz#e858dd98358ccd864b65d53ab0fb682cd5e96b89"
|
||||
@@ -13276,6 +13406,37 @@ react-stately@3.34.0:
|
||||
"@react-stately/tree" "^3.8.6"
|
||||
"@react-types/shared" "^3.26.0"
|
||||
|
||||
react-stately@3.35.0, react-stately@^3.35.0:
|
||||
version "3.35.0"
|
||||
resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.35.0.tgz#92bfc83bb4f7626a57c6aeabe4d08aeaab1fa2f7"
|
||||
integrity sha512-1BH21J/TOHpyZe7c+f1BU2bnRWaBDTjLH0WdBuzNfPOXu7RBG3ebPIRvqd7UkPaVfIcol2QJnxe8S0a314JWKA==
|
||||
dependencies:
|
||||
"@react-stately/calendar" "^3.7.0"
|
||||
"@react-stately/checkbox" "^3.6.11"
|
||||
"@react-stately/collections" "^3.12.1"
|
||||
"@react-stately/color" "^3.8.2"
|
||||
"@react-stately/combobox" "^3.10.2"
|
||||
"@react-stately/data" "^3.12.1"
|
||||
"@react-stately/datepicker" "^3.12.0"
|
||||
"@react-stately/disclosure" "^3.0.1"
|
||||
"@react-stately/dnd" "^3.5.1"
|
||||
"@react-stately/form" "^3.1.1"
|
||||
"@react-stately/list" "^3.11.2"
|
||||
"@react-stately/menu" "^3.9.1"
|
||||
"@react-stately/numberfield" "^3.9.9"
|
||||
"@react-stately/overlays" "^3.6.13"
|
||||
"@react-stately/radio" "^3.10.10"
|
||||
"@react-stately/searchfield" "^3.5.9"
|
||||
"@react-stately/select" "^3.6.10"
|
||||
"@react-stately/selection" "^3.19.0"
|
||||
"@react-stately/slider" "^3.6.1"
|
||||
"@react-stately/table" "^3.13.1"
|
||||
"@react-stately/tabs" "^3.7.1"
|
||||
"@react-stately/toggle" "^3.8.1"
|
||||
"@react-stately/tooltip" "^3.5.1"
|
||||
"@react-stately/tree" "^3.8.7"
|
||||
"@react-types/shared" "^3.27.0"
|
||||
|
||||
react-stately@^3.34.0:
|
||||
version "3.36.0"
|
||||
resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.36.0.tgz#1544f0a742145d9bc2d67a8c76af3648a9982fd6"
|
||||
@@ -13308,37 +13469,6 @@ react-stately@^3.34.0:
|
||||
"@react-stately/tree" "^3.8.8"
|
||||
"@react-types/shared" "^3.28.0"
|
||||
|
||||
react-stately@^3.35.0:
|
||||
version "3.35.0"
|
||||
resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.35.0.tgz#92bfc83bb4f7626a57c6aeabe4d08aeaab1fa2f7"
|
||||
integrity sha512-1BH21J/TOHpyZe7c+f1BU2bnRWaBDTjLH0WdBuzNfPOXu7RBG3ebPIRvqd7UkPaVfIcol2QJnxe8S0a314JWKA==
|
||||
dependencies:
|
||||
"@react-stately/calendar" "^3.7.0"
|
||||
"@react-stately/checkbox" "^3.6.11"
|
||||
"@react-stately/collections" "^3.12.1"
|
||||
"@react-stately/color" "^3.8.2"
|
||||
"@react-stately/combobox" "^3.10.2"
|
||||
"@react-stately/data" "^3.12.1"
|
||||
"@react-stately/datepicker" "^3.12.0"
|
||||
"@react-stately/disclosure" "^3.0.1"
|
||||
"@react-stately/dnd" "^3.5.1"
|
||||
"@react-stately/form" "^3.1.1"
|
||||
"@react-stately/list" "^3.11.2"
|
||||
"@react-stately/menu" "^3.9.1"
|
||||
"@react-stately/numberfield" "^3.9.9"
|
||||
"@react-stately/overlays" "^3.6.13"
|
||||
"@react-stately/radio" "^3.10.10"
|
||||
"@react-stately/searchfield" "^3.5.9"
|
||||
"@react-stately/select" "^3.6.10"
|
||||
"@react-stately/selection" "^3.19.0"
|
||||
"@react-stately/slider" "^3.6.1"
|
||||
"@react-stately/table" "^3.13.1"
|
||||
"@react-stately/tabs" "^3.7.1"
|
||||
"@react-stately/toggle" "^3.8.1"
|
||||
"@react-stately/tooltip" "^3.5.1"
|
||||
"@react-stately/tree" "^3.8.7"
|
||||
"@react-types/shared" "^3.27.0"
|
||||
|
||||
react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
|
||||
@@ -13366,6 +13496,14 @@ react-transition-group@^4.3.0:
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-window@^1.8.11:
|
||||
version "1.8.11"
|
||||
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.11.tgz#a857b48fa85bd77042d59cc460964ff2e0648525"
|
||||
integrity sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
memoize-one ">=3.1.1 <6"
|
||||
|
||||
react@*, react@19.0.0:
|
||||
version "19.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd"
|
||||
@@ -13408,6 +13546,18 @@ redent@^3.0.0:
|
||||
indent-string "^4.0.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
redux@^4.1.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
|
||||
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
|
||||
redux@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
|
||||
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
|
||||
|
||||
reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9"
|
||||
@@ -14943,7 +15093,7 @@ typed-array-length@^1.0.7:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
reflect.getprototypeof "^1.0.6"
|
||||
|
||||
typescript@*, typescript@5.8.2, typescript@^5.0.4:
|
||||
typescript@*, typescript@^5.0.4:
|
||||
version "5.8.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
|
||||
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
|
||||
@@ -15244,6 +15394,11 @@ utils-merge@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
uuid@11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912"
|
||||
integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
|
||||
|
||||
uuid@^11.0.3:
|
||||
version "11.0.5"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.5.tgz#07b46bdfa6310c92c3fb3953a8720f170427fc62"
|
||||
@@ -15960,7 +16115,7 @@ yargs@17.7.2, yargs@^17.3.1:
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
yjs@*, yjs@13.6.23, yjs@^13.6.15:
|
||||
yjs@*, yjs@^13.6.15:
|
||||
version "13.6.23"
|
||||
resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.23.tgz#62358dfa52e92dc870b8a0bedcf0d4cbd4c5ffa8"
|
||||
integrity sha512-ExtnT5WIOVpkL56bhLeisG/N5c4fmzKn4k0ROVfJa5TY2QHbH7F0Wu2T5ZhR7ErsFWQEFafyrnSI8TPKVF9Few==
|
||||
|
||||
@@ -124,6 +124,7 @@ yProvider:
|
||||
tag: "latest"
|
||||
|
||||
envVars:
|
||||
COLLABORATION_BACKEND_BASE_URL: https://impress.127.0.0.1.nip.io
|
||||
COLLABORATION_LOGGING: true
|
||||
COLLABORATION_SERVER_ORIGIN: https://impress.127.0.0.1.nip.io
|
||||
COLLABORATION_SERVER_SECRET: my-secret
|
||||
@@ -136,9 +137,6 @@ ingress:
|
||||
ingressCollaborationWS:
|
||||
enabled: true
|
||||
host: impress.127.0.0.1.nip.io
|
||||
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-url: https://impress.127.0.0.1.nip.io/api/v1.0/documents/collaboration-auth/
|
||||
|
||||
ingressCollaborationApi:
|
||||
enabled: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
environments:
|
||||
dev:
|
||||
values:
|
||||
- version: 2.6.0
|
||||
- version: 3.0.0
|
||||
---
|
||||
repositories:
|
||||
- name: bitnami
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: docs
|
||||
version: 2.6.0
|
||||
version: 3.0.0
|
||||
appVersion: latest
|
||||
|
||||
@@ -75,15 +75,11 @@ ingressCollaborationWS:
|
||||
## @param ingressCollaborationWS.customBackends Add custom backends to ingress
|
||||
customBackends: []
|
||||
|
||||
## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/auth-response-headers
|
||||
## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/auth-url
|
||||
## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/enable-websocket
|
||||
## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/proxy-read-timeout
|
||||
## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/proxy-send-timeout
|
||||
## @param ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/upstream-hash-by
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Can-Edit, X-User-Id"
|
||||
nginx.ingress.kubernetes.io/auth-url: https://impress.example.com/api/v1.0/documents/collaboration-auth/
|
||||
nginx.ingress.kubernetes.io/enable-websocket: "true"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "86400"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "86400"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "2.6.0",
|
||||
"version": "3.0.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user