diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml index a77c5a17..5871ac90 100644 --- a/.github/workflows/docker-hub.yml +++ b/.github/workflows/docker-hub.yml @@ -48,12 +48,12 @@ jobs: with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - - name: Run trivy scan - uses: numerique-gouv/action-trivy-cache@main - with: - docker-build-args: '--target backend-production -f Dockerfile' - docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}' +# - +# name: Run trivy scan +# uses: numerique-gouv/action-trivy-cache@main +# with: +# docker-build-args: '--target backend-production -f Dockerfile' +# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}' - name: Build and push uses: docker/build-push-action@v6 @@ -93,12 +93,12 @@ jobs: with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - - name: Run trivy scan - uses: numerique-gouv/action-trivy-cache@main - with: - docker-build-args: '-f src/frontend/Dockerfile --target frontend-production' - docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}' +# - +# name: Run trivy scan +# uses: numerique-gouv/action-trivy-cache@main +# with: +# docker-build-args: '-f src/frontend/Dockerfile --target frontend-production' +# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}' - name: Build and push uses: docker/build-push-action@v6 @@ -139,12 +139,12 @@ jobs: with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - - name: Run trivy scan - uses: numerique-gouv/action-trivy-cache@main - with: - docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production' - docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}' +# - +# name: Run trivy scan +# uses: numerique-gouv/action-trivy-cache@main +# with: +# docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production' +# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}' - name: Build and push uses: docker/build-push-action@v6 @@ -185,13 +185,13 @@ jobs: with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - - name: Run trivy scan - uses: numerique-gouv/action-trivy-cache@main - continue-on-error: true - with: - docker-build-args: '-f src/summary/Dockerfile --target production' - docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}' +# - +# name: Run trivy scan +# uses: numerique-gouv/action-trivy-cache@main +# continue-on-error: true +# with: +# docker-build-args: '-f src/summary/Dockerfile --target production' +# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}' docker-context: './src/summary' - name: Build and push @@ -233,14 +233,14 @@ jobs: with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - - name: Run trivy scan - uses: numerique-gouv/action-trivy-cache@main - continue-on-error: true - with: - docker-build-args: '-f src/agents/Dockerfile --target production' - docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}' - docker-context: './src/agents' +# - +# name: Run trivy scan +# uses: numerique-gouv/action-trivy-cache@main +# continue-on-error: true +# with: +# docker-build-args: '-f src/agents/Dockerfile --target production' +# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}' +# docker-context: './src/agents' - name: Build and push uses: docker/build-push-action@v6 diff --git a/.github/workflows/meet.yml b/.github/workflows/meet.yml index 78894582..f924c982 100644 --- a/.github/workflows/meet.yml +++ b/.github/workflows/meet.yml @@ -297,46 +297,6 @@ jobs: - name: Run tests run: uv run pytest -n 2 - test-summary: - runs-on: ubuntu-latest - permissions: - contents: read - defaults: - run: - working-directory: src/summary - - env: - APP_API_TOKEN: "test-api-token" - AWS_STORAGE_BUCKET_NAME: "http://meet-media-storage" - AWS_S3_ENDPOINT_URL: "minio:9000" - AWS_S3_ACCESS_KEY_ID: "meet" - AWS_S3_SECRET_ACCESS_KEY: "password" - WHISPERX_BASE_URL: "https://configure-your-url.com" - WHISPERX_ASR_MODEL: "large-v2" - WHISPERX_API_KEY: "test-whisperx-secret" - WHISPERX_DEFAULT_LANGUAGE: "fr" - LLM_BASE_URL: "https://configure-your-url.com" - LLM_API_KEY: "test-llm-secret" - LLM_MODEL: "test-llm-model" - WEBHOOK_API_TOKEN: "test-webhook-secret" - WEBHOOK_URL: "https://configure-your-url.com" - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Install Python - uses: actions/setup-python@v6 - with: - python-version: "3.13" - cache: "pip" - - - name: Install development dependencies - run: pip install --user .[dev] - - - name: Run summary tests - run: ~/.local/bin/pytest - lint-front: runs-on: ubuntu-latest permissions: diff --git a/CHANGELOG.md b/CHANGELOG.md index 18cbbff7..9e2c67f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,23 +8,6 @@ and this project adheres to ## [Unreleased] -### Changed - -- ♿️(frontend) fix sidepanel accessibility aria-label #1182 -- ♿️(frontend) fix more tools heading hierarchy #1181 -- ♿️(fronted) improve button descriptions for More tools actions #1184 -- 💄(spinner) enforce spinner height #1183 -- 💄(custom-background) add upload indicator with preview #1183 -- ♿️(backend) improve logo accessibility in recording email notification #1092 -- ♿️(summary) improve accessibility of transcription download link #1187 - -### Fixed - -- 🐛(frontend) disable personal custom background while deleting #1183 -- 🐛(frontend) auto-select new custom background when not logged in #1183 - -## [1.11.0] - 2026-03-19 - ### Added - ✨(helm) support celery with our Django backend #1124 @@ -32,24 +15,20 @@ and this project adheres to - ✨(backend) add authenticated user rate throttling on request-entry #1129 - ✨(backend) expose `is_active` field for Application in Django admin #1133 - ✨(file-upload) disable by default & limit count by user #1141 -- ✨(frontend) custom background #1067 ### Changed - ♿️(frontend) Caption text size setting for accessibility #1062 - ♿️(frontend) sync html lang attribute with i18n for screen readers #1111 - ♿️(frontend) improve MoreLink a11y and UX on home page #1112 -- ♿️(frontend) improve chat toast a11y for screen readers #1109 -- ♿️(frontend) improve ui and aria labels for help article links #1108 +- ♿(frontend) improve chat toast a11y for screen readers #1109 +- ♿(frontend) improve ui and aria labels for help article links #1108 - 🌐(frontend) improve German translation #1125 - 🔨(python-env) migrate meet main app to UV #1120 - ♻️(backend) align Application model field with `is_active` convention #1133 - 🔐(backend) avoids revealing the inactive status of an application #1135 - ⚡️(helm) reduce initialDelaySeconds and add periods seconds #1139 - 🔒️(backend) avoid information exposure through exception messages #1144 -- ⬆️(dependencies) update PyJWT to v2.12.0 [SECURITY] #1151 -- 📌(agents) unpin OpenSSL and related dependencies #1167 -- ♿️(frontend) add caption font and background color customization #1122 ### Fixed @@ -57,7 +36,6 @@ and this project adheres to - 🩹(backend) add page_size to pagination for room endpoints #1131 - 🐛(backend) refactor lobby throttling to use participant id #1129 - 🩹(backend) ignore non-recording uploads in storage webhook handler #1142 -- 🐛(frontend) fix dimension mismatch in BackgroundCustomProcessor #1116 ## [1.10.0] - 2026-03-05 diff --git a/Makefile b/Makefile index 5c9504d5..b044a718 100644 --- a/Makefile +++ b/Makefile @@ -191,7 +191,6 @@ lint-pylint: ## lint back-end python sources with pylint only on changed files f test: ## run project tests @$(MAKE) test-back-parallel - @$(MAKE) test-summary .PHONY: test test-back: ## run back-end tests @@ -204,11 +203,6 @@ test-back-parallel: ## run all back-end tests in parallel bin/pytest -n auto $${args:-${1}} .PHONY: test-back-parallel -test-summary: ## run summary tests - @args="$(filter-out $@,$(MAKECMDGOALS))" && \ - bin/pytest-summary $${args:-${1}} -.PHONY: test-summary - makemigrations: ## run django makemigrations for the Meet project. @echo "$(BOLD)Running makemigrations$(RESET)" @$(COMPOSE) up -d postgresql diff --git a/bin/pytest-summary b/bin/pytest-summary deleted file mode 100755 index 29e32101..00000000 --- a/bin/pytest-summary +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source "$(dirname "${BASH_SOURCE[0]}")/_config.sh" - -_dc_run \ - app-summary-dev \ - python -m pytest "$@" diff --git a/docs/examples/compose/keycloak/README.md b/docs/examples/compose/keycloak/README.md index 79e8d6f4..dff2d5d6 100644 --- a/docs/examples/compose/keycloak/README.md +++ b/docs/examples/compose/keycloak/README.md @@ -61,10 +61,11 @@ services: `docker compose up -d` ``` -Your keycloak instance is now available on https://id.yourdomain.tld +Your keycloak instance is now available on https://doc.yourdomain.tld > [!CAUTION] > Version of the images are set to latest, you should pin it to the desired version to avoid unwanted upgrades when pulling latest image. You can find available versions on [Keycloak registry](https://quay.io/repository/keycloak/keycloak?tab=tags). +``` ## Creating an OIDC Client for Meet Application @@ -75,7 +76,7 @@ Your keycloak instance is now available on https://id.yourdomain.tld 3. Enter the name of the realm - `meet`. 4. Click "Create". -### Step 2: Create a New Client +#### Step 2: Create a New Client 1. Navigate to the "Clients" tab. 2. Click on the "Create client" button. @@ -85,7 +86,7 @@ Your keycloak instance is now available on https://id.yourdomain.tld 1. Set the "Web Origins" to the URL of your meet application - e.g. `https://meet.example.com`. 1. Click "Save". -### Step 3: Get Client Credentials +#### Step 3: Get Client Credentials 1. Go to the "Credentials" tab. 2. Copy the client ID (`meet` in this example) and the client secret. diff --git a/env.d/development/common.dist b/env.d/development/common.dist index 4dcc335c..f3fe3e00 100644 --- a/env.d/development/common.dist +++ b/env.d/development/common.dist @@ -27,7 +27,7 @@ AWS_S3_DOMAIN_REPLACE=http://localhost:9000 AWS_S3_ENDPOINT_URL=http://minio:9000 AWS_S3_ACCESS_KEY_ID=meet AWS_S3_SECRET_ACCESS_KEY=password -MEDIA_BASE_URL=http://localhost:3000 +MEDIA_BASE_URL=http://localhost:8083 FILE_UPLOAD_ENABLED=True # OIDC diff --git a/src/agents/Dockerfile b/src/agents/Dockerfile index 50c29007..f6c3c652 100644 --- a/src/agents/Dockerfile +++ b/src/agents/Dockerfile @@ -4,6 +4,8 @@ FROM python:3.13-slim AS base RUN apt-get update && apt-get install -y \ libglib2.0-0 \ libgobject-2.0-0 \ + "openssl=3.5.4-1~deb13u2" \ + "libssl3t64=3.5.4-1~deb13u2" \ && rm -rf /var/lib/apt/lists/* FROM base AS builder diff --git a/src/agents/pyproject.toml b/src/agents/pyproject.toml index d3e033e3..cc00dcf9 100644 --- a/src/agents/pyproject.toml +++ b/src/agents/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "agents" -version = "1.11.0" +version = "1.10.0" requires-python = ">=3.12" dependencies = [ "livekit-agents==1.3.10", diff --git a/src/backend/core/api/throttling.py b/src/backend/core/api/throttling.py index b7b89b43..e0a8e97a 100644 --- a/src/backend/core/api/throttling.py +++ b/src/backend/core/api/throttling.py @@ -73,3 +73,9 @@ class CreationCallbackAnonRateThrottle(MonitoredAnonRateThrottle): """Throttle Anonymous user requesting room generation callback""" scope = "creation_callback" + + +class SessionExchangeAnonRateThrottle(MonitoredAnonRateThrottle): + """Throttle anonymous requests to the session exchange endpoint.""" + + scope = "session_exchange" diff --git a/src/backend/core/authentication/api.py b/src/backend/core/authentication/api.py new file mode 100644 index 00000000..5446689d --- /dev/null +++ b/src/backend/core/authentication/api.py @@ -0,0 +1,61 @@ +"""API endpoint for exchanging a one-time code for a session ID.""" + +import logging + +from django.conf import settings +from django.core.cache import cache + +from rest_framework import serializers, status +from rest_framework.decorators import api_view, permission_classes, throttle_classes +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + +from core.api.throttling import SessionExchangeAnonRateThrottle + +from .views import EXCHANGE_CODE_PREFIX + +logger = logging.getLogger(__name__) + + +class SessionExchangeSerializer(serializers.Serializer): + """Validates the exchange code request.""" + + code = serializers.CharField(max_length=64, min_length=16) + + +@api_view(["POST"]) +@permission_classes([AllowAny]) +@throttle_classes([SessionExchangeAnonRateThrottle]) +def session_exchange( + request, +): # NOSONAR (S3752) POST-only, AllowAny is intentional: single-use code with 30s TTL and rate limiting + """Exchange a one-time code for a session ID. + + The code was generated during the OIDC callback and stored in cache + with a short TTL. This endpoint retrieves the session ID, deletes the + code (single-use), and returns the session ID to the native app. + """ + serializer = SessionExchangeSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + code = serializer.validated_data["code"] + cache_key = f"{EXCHANGE_CODE_PREFIX}{code}" + + session_key = cache.get(cache_key) + if session_key is None: + logger.warning( + "Session exchange failed: invalid or expired code from %s", + request.META.get("REMOTE_ADDR"), + ) + return Response( + {"detail": "Invalid or expired code."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Delete immediately — single use + cache.delete(cache_key) + + cookie_name = getattr(settings, "SESSION_COOKIE_NAME", "sessionid") + logger.info("Session exchange successful from %s", request.META.get("REMOTE_ADDR")) + + return Response({cookie_name: session_key}) diff --git a/src/backend/core/authentication/views.py b/src/backend/core/authentication/views.py new file mode 100644 index 00000000..4dfa62d3 --- /dev/null +++ b/src/backend/core/authentication/views.py @@ -0,0 +1,103 @@ +"""Custom OIDC authentication views for native app support. + +When a native app (iOS, Android, Desktop) initiates OIDC login, it sets +`returnTo` to a custom URL scheme (e.g. `visio://auth-callback`). After +the OIDC flow completes, Django sets the session cookie and redirects to +that URL. However, native apps cannot read browser cookies — they need +the session ID passed explicitly. + +Instead of exposing the session ID directly in the redirect URL, this +module generates a short-lived, single-use exchange code. The native app +then exchanges this code for the session ID via a dedicated API endpoint. +""" + +import uuid +from urllib.parse import urlencode, urlparse + +from django.conf import settings +from django.core.cache import cache +from django.http import HttpResponseRedirect + +from lasuite.oidc_login.views import ( + OIDCAuthenticationCallbackView as BaseCallbackView, + OIDCAuthenticationRequestView as BaseRequestView, +) + +# Cache key prefix and TTL for exchange codes +EXCHANGE_CODE_PREFIX = "auth_exchange:" +EXCHANGE_CODE_TTL = 30 # seconds + + +class NativeAppRedirect(HttpResponseRedirect): + """HttpResponseRedirect subclass that allows native app custom URL schemes. + + Django's HttpResponseRedirect only allows http, https, and ftp schemes. + Native apps use custom schemes (e.g. visio://) for deep links, which + Django rejects with DisallowedRedirect. This subclass extends + allowed_schemes with the configured native app schemes. + """ + + allowed_schemes = HttpResponseRedirect.allowed_schemes + list( + getattr(settings, "NATIVE_APP_REDIRECT_SCHEMES", []) + ) + + +class OIDCAuthenticationRequestView(BaseRequestView): + """Custom authenticate view that preserves native app returnTo in session. + + mozilla-django-oidc's get_next_url() rejects custom URL schemes + (e.g. visio://auth-callback) because url_has_allowed_host_and_scheme() + only allows http/https. We intercept the returnTo parameter and store + it directly in the session for whitelisted schemes, bypassing the + safety check (which is not relevant for native app deep links). + """ + + def get(self, request): + redirect_field = getattr(settings, "OIDC_REDIRECT_FIELD_NAME", "returnTo") + return_to = request.GET.get(redirect_field, "") + parsed = urlparse(return_to) + allowed_schemes = getattr(settings, "NATIVE_APP_REDIRECT_SCHEMES", []) + + response = super().get(request) + + # Override oidc_login_next AFTER super() which set it to None + # via get_next_url() rejecting the custom scheme. + if parsed.scheme in allowed_schemes: + request.session["oidc_login_next"] = return_to + request.session.save() + + return response + + +class OIDCAuthenticationCallbackView(BaseCallbackView): + """Callback view that generates an exchange code for native app deep links.""" + + def login_success(self): + """After successful login, append exchange code for whitelisted scheme redirects.""" + # Temporarily remove native redirect from session to prevent + # super().login_success() from raising DisallowedRedirect when + # it tries HttpResponseRedirect with a custom scheme. + native_redirect = self.request.session.pop("oidc_login_next", None) + allowed_schemes = getattr(settings, "NATIVE_APP_REDIRECT_SCHEMES", []) + parsed = urlparse(native_redirect or "") + + if native_redirect and parsed.scheme in allowed_schemes: + # Let super() redirect to the default URL (homepage) + super().login_success() + + # Generate a short-lived, single-use exchange code + exchange_code = uuid.uuid4().hex + session_key = self.request.session.session_key + cache.set( + f"{EXCHANGE_CODE_PREFIX}{exchange_code}", + session_key, + EXCHANGE_CODE_TTL, + ) + + separator = "&" if parsed.query else "?" + new_url = ( + f"{native_redirect}{separator}{urlencode({'code': exchange_code})}" + ) + return NativeAppRedirect(new_url) + + return super().login_success() diff --git a/src/backend/core/tests/authentication/test_session_exchange.py b/src/backend/core/tests/authentication/test_session_exchange.py new file mode 100644 index 00000000..e4bdb9db --- /dev/null +++ b/src/backend/core/tests/authentication/test_session_exchange.py @@ -0,0 +1,143 @@ +""" +Tests for the session exchange API endpoint and OIDC callback view. +""" + +import uuid + +from django.core.cache import cache + +import pytest +from rest_framework.test import APIClient + +from core.authentication.views import EXCHANGE_CODE_PREFIX, EXCHANGE_CODE_TTL + +pytestmark = pytest.mark.django_db + + +@pytest.fixture(autouse=True) +def _clear_throttle_cache(): + """Clear cache before each test to reset throttle counters.""" + cache.clear() + + +def test_session_exchange_valid_code(): + """A valid exchange code should return the session ID and be consumed.""" + code = uuid.uuid4().hex + cache_key = f"{EXCHANGE_CODE_PREFIX}{code}" + cache.set(cache_key, "test-session-key-123", EXCHANGE_CODE_TTL) + + client = APIClient() + response = client.post( + "/api/v1.0/auth/session-exchange/", + {"code": code}, + format="json", + ) + + assert response.status_code == 200 + data = response.json() + assert "test-session-key-123" in data.values() + + # Code should be consumed (single-use) + assert cache.get(cache_key) is None + + +def test_session_exchange_invalid_code(): + """An invalid/unknown code should return 400.""" + client = APIClient() + response = client.post( + "/api/v1.0/auth/session-exchange/", + {"code": uuid.uuid4().hex}, + format="json", + ) + + assert response.status_code == 400 + assert response.json()["detail"] == "Invalid or expired code." + + +def test_session_exchange_expired_code(): + """An expired code should return 400.""" + code = uuid.uuid4().hex + cache_key = f"{EXCHANGE_CODE_PREFIX}{code}" + # Set with 0 TTL to simulate expiration + cache.set(cache_key, "expired-session", 0) + + client = APIClient() + response = client.post( + "/api/v1.0/auth/session-exchange/", + {"code": code}, + format="json", + ) + + assert response.status_code == 400 + + +def test_session_exchange_code_too_short(): + """A code that's too short should be rejected by validation.""" + client = APIClient() + response = client.post( + "/api/v1.0/auth/session-exchange/", + {"code": "short"}, + format="json", + ) + + assert response.status_code == 400 + + +def test_session_exchange_missing_code(): + """Missing code field should be rejected.""" + client = APIClient() + response = client.post( + "/api/v1.0/auth/session-exchange/", + {}, + format="json", + ) + + assert response.status_code == 400 + + +def test_session_exchange_replay_attack(): + """Using the same code twice should fail the second time.""" + code = uuid.uuid4().hex + cache.set(f"{EXCHANGE_CODE_PREFIX}{code}", "session-123", EXCHANGE_CODE_TTL) + + client = APIClient() + + # First use succeeds + response = client.post( + "/api/v1.0/auth/session-exchange/", + {"code": code}, + format="json", + ) + assert response.status_code == 200 + + # Second use fails + response = client.post( + "/api/v1.0/auth/session-exchange/", + {"code": code}, + format="json", + ) + assert response.status_code == 400 + + +def test_session_exchange_returns_correct_cookie_name(settings): + """The response key should match SESSION_COOKIE_NAME from settings.""" + settings.SESSION_COOKIE_NAME = "meet_sessionid" + code = uuid.uuid4().hex + cache.set(f"{EXCHANGE_CODE_PREFIX}{code}", "my-session", EXCHANGE_CODE_TTL) + + client = APIClient() + response = client.post( + "/api/v1.0/auth/session-exchange/", + {"code": code}, + format="json", + ) + + assert response.status_code == 200 + assert response.json() == {"meet_sessionid": "my-session"} + + +def test_session_exchange_get_not_allowed(): + """GET method should not be allowed on the exchange endpoint.""" + client = APIClient() + response = client.get("/api/v1.0/auth/session-exchange/") + assert response.status_code == 405 diff --git a/src/backend/core/urls.py b/src/backend/core/urls.py index caa75fea..02fb676d 100644 --- a/src/backend/core/urls.py +++ b/src/backend/core/urls.py @@ -7,6 +7,7 @@ from lasuite.oidc_login.urls import urlpatterns as oidc_urls from rest_framework.routers import DefaultRouter from core.api import get_frontend_configuration, viewsets +from core.authentication.api import session_exchange from core.external_api import viewsets as external_viewsets # - Main endpoints @@ -40,6 +41,9 @@ urlpatterns = [ [ *router.urls, *oidc_urls, + path( + "auth/session-exchange/", session_exchange, name="session_exchange" + ), path("config/", get_frontend_configuration, name="config"), ] ), diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index f6768ea0..f03d09d1 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -344,6 +344,11 @@ class Base(Configuration): environ_name="CREATION_CALLBACK_THROTTLE_RATES", environ_prefix=None, ), + "session_exchange": values.Value( + default="5/minute", + environ_name="SESSION_EXCHANGE_THROTTLE_RATES", + environ_prefix=None, + ), }, } MONITORED_THROTTLE_FAILURE_CALLBACK = ( @@ -459,8 +464,16 @@ class Base(Configuration): ) # OIDC - Authorization Code Flow - OIDC_AUTHENTICATE_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationRequestView" - OIDC_CALLBACK_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationCallbackView" + OIDC_AUTHENTICATE_CLASS = "core.authentication.views.OIDCAuthenticationRequestView" + OIDC_CALLBACK_CLASS = "core.authentication.views.OIDCAuthenticationCallbackView" + + # Custom URL schemes allowed for native app OIDC redirects. + # Only these schemes will receive an exchange code in the callback. + NATIVE_APP_REDIRECT_SCHEMES = values.ListValue( + default=["visio"], + environ_name="NATIVE_APP_REDIRECT_SCHEMES", + environ_prefix=None, + ) OIDC_CREATE_USER = values.BooleanValue( default=True, environ_name="OIDC_CREATE_USER", environ_prefix=None ) diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 1f775714..ca48cc41 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "uv_build" [project] name = "meet" -version = "1.11.0" +version = "1.10.0" authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -52,7 +52,7 @@ dependencies = [ "nested-multipart-parser==1.6.0", "psycopg[binary]==3.3.2", "pydantic==2.12.4", - "PyJWT==2.12.0", + "PyJWT==2.11.0", "python-frontmatter==1.1.0", "python-magic==0.4.27", "requests==2.32.5", diff --git a/src/backend/uv.lock b/src/backend/uv.lock index cc9f561c..87ad33b1 100644 --- a/src/backend/uv.lock +++ b/src/backend/uv.lock @@ -1157,7 +1157,7 @@ wheels = [ [[package]] name = "meet" -version = "1.11.0" +version = "1.10.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -1251,7 +1251,7 @@ requires-dist = [ { name = "nested-multipart-parser", specifier = "==1.6.0" }, { name = "psycopg", extras = ["binary"], specifier = "==3.3.2" }, { name = "pydantic", specifier = "==2.12.4" }, - { name = "pyjwt", specifier = "==2.12.0" }, + { name = "pyjwt", specifier = "==2.11.0" }, { name = "python-frontmatter", specifier = "==1.1.0" }, { name = "python-magic", specifier = "==0.4.27" }, { name = "redis", specifier = "==5.2.1" }, @@ -1754,11 +1754,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.12.0" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a8/10/e8192be5f38f3e8e7e046716de4cae33d56fd5ae08927a823bb916be36c1/pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02", size = 102511, upload-time = "2026-03-12T17:15:30.831Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/70/70f895f404d363d291dcf62c12c85fdd47619ad9674ac0f53364d035925a/pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e", size = 29700, upload-time = "2026-03-12T17:15:29.257Z" }, + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] [[package]] diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 3d09434d..85cf05d1 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "meet", - "version": "1.11.0", + "version": "1.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meet", - "version": "1.11.0", + "version": "1.10.0", "dependencies": { "@fontsource-variable/material-symbols-outlined": "5.2.34", "@fontsource/material-icons-outlined": "5.2.6", diff --git a/src/frontend/package.json b/src/frontend/package.json index af197f05..74dedb1f 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -1,7 +1,7 @@ { "name": "meet", "private": true, - "version": "1.11.0", + "version": "1.10.0", "type": "module", "scripts": { "dev": "panda codegen && vite", diff --git a/src/frontend/public/assets/logo-suite-numerique.png b/src/frontend/public/assets/logo-suite-numerique.png index b4e95d5b..840e972e 100644 Binary files a/src/frontend/public/assets/logo-suite-numerique.png and b/src/frontend/public/assets/logo-suite-numerique.png differ diff --git a/src/frontend/src/api/fetchApi.ts b/src/frontend/src/api/fetchApi.ts index a3c5039d..3514b9b9 100644 --- a/src/frontend/src/api/fetchApi.ts +++ b/src/frontend/src/api/fetchApi.ts @@ -15,19 +15,7 @@ export const fetchApi = async >( ...options?.headers, }, }) - - let result: T - if (response.status === 204) { - result = undefined as T - } else { - const contentType = response.headers.get('content-type') ?? '' - if (!contentType.includes('application/json')) { - result = undefined as T - } else { - result = (await response.json()) as T - } - } - + const result = await response.json() if (!response.ok) { throw new ApiError(response.status, result) } diff --git a/src/frontend/src/api/queryKeys.ts b/src/frontend/src/api/queryKeys.ts index 13f2805e..ccb21d29 100644 --- a/src/frontend/src/api/queryKeys.ts +++ b/src/frontend/src/api/queryKeys.ts @@ -5,5 +5,4 @@ export const keys = { requestEntry: 'requestEntry', waitingParticipants: 'waitingParticipants', roomCreationCallback: 'roomCreationCallback', - files: 'files', } diff --git a/src/frontend/src/api/useConfig.ts b/src/frontend/src/api/useConfig.ts index e2a255e9..67a67738 100644 --- a/src/frontend/src/api/useConfig.ts +++ b/src/frontend/src/api/useConfig.ts @@ -30,13 +30,6 @@ export interface ApiConfig { expiration_days?: number max_duration?: number } - background_image: { - upload_is_enabled: boolean - max_size: number - max_count_by_user: number - allowed_extensions: string[] - allowed_mimetypes: string[] - } subtitle: { enabled: boolean } diff --git a/src/frontend/src/features/files/api/createFile.ts b/src/frontend/src/features/files/api/createFile.ts deleted file mode 100644 index ffccdac2..00000000 --- a/src/frontend/src/features/files/api/createFile.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { fetchApi } from '@/api/fetchApi' -import { useMutation } from '@tanstack/react-query' -import { ApiFileItem } from '@/features/files/api/types.ts' -import { keys } from '@/api/queryKeys.ts' -import { queryClient } from '@/api/queryClient.ts' - -/** - * Upload a file, using XHR so we can report on progress through a handler. - * - * @param url The URL to PUT the file to. - * @param file The file to upload. - * @param progressHandler A handler that receives progress updates as a single integer `0 <= x <= 100`. - */ -export const uploadFile = ( - url: string, - file: File, - progressHandler: (progress: number) => void -) => - new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest() - xhr.open('PUT', url) - xhr.setRequestHeader('X-amz-acl', 'private') - xhr.setRequestHeader('Content-Type', file.type) - - xhr.addEventListener('error', reject) - xhr.addEventListener('abort', reject) - - xhr.addEventListener('readystatechange', () => { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - // Make sure to always set the progress to 100% when the upload is done. - // Because 'progress' event listener is not called when the file size is 0. - progressHandler(100) - return resolve(true) - } - reject(new Error(`Failed to perform the upload on ${url}.`)) - } - }) - - xhr.upload.addEventListener('progress', (progressEvent) => { - if (progressEvent.lengthComputable) { - progressHandler( - Math.floor((progressEvent.loaded / progressEvent.total) * 100) - ) - } - }) - - xhr.send(file) - }) - -/** - * Asynchronously creates a new file and uploads it to the server. - * - * @param {object} params - The parameters for the file creation and upload process. - * @param {File} params.file - The file object to be uploaded. - * @param {function} params.onProgress - A callback function that receives the upload progress as a number (0 to 100). - * @returns {Promise} A promise that resolves when the file has been successfully uploaded and the server process is completed. - */ -export const createFile = async ({ - file, - onProgress, -}: { - file: File - onProgress: (progress: number) => void -}): Promise => { - const res = await fetchApi(`/files/`, { - method: 'POST', - body: JSON.stringify({ filename: file.name, type: 'background_image' }), - }) - if (res.upload_state !== 'pending') { - throw new Error('State should be pending right after creation') - } - const policy = res.policy - await uploadFile(policy, file, onProgress) - const createdFile = await fetchApi( - `/files/${res.id}/upload-ended/`, - { - method: 'POST', - } - ) - - // We invalidate the files query to make sure the new file is immediately available. - await queryClient.invalidateQueries({ - queryKey: [keys.files], - }) - return createdFile -} - -export const useCreateFile = () => { - return useMutation({ - mutationFn: createFile, - }) -} diff --git a/src/frontend/src/features/files/api/deleteFile.ts b/src/frontend/src/features/files/api/deleteFile.ts deleted file mode 100644 index 0fdd2bdc..00000000 --- a/src/frontend/src/features/files/api/deleteFile.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { fetchApi } from '@/api/fetchApi' -import { useMutation, useQueryClient } from '@tanstack/react-query' -import { keys } from '@/api/queryKeys.ts' - -/** - * Deletes a file specified by its unique identifier. - * - * @param {Object} params - The parameters required for deleting the file. - * @param {string} params.fileId - The unique identifier of the file to be deleted. - * @returns {Promise} A promise that resolves when the file is successfully deleted. - */ -export const deleteFile = async ({ - fileId, -}: { - fileId: string -}): Promise => { - await fetchApi(`/files/${fileId}/`, { - method: 'DELETE', - }) -} - -export const useDeleteFile = () => { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: deleteFile, - onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: [keys.files], - }) - }, - }) -} diff --git a/src/frontend/src/features/files/api/listFiles.ts b/src/frontend/src/features/files/api/listFiles.ts deleted file mode 100644 index db57a4a1..00000000 --- a/src/frontend/src/features/files/api/listFiles.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { fetchApi } from '@/api/fetchApi' -import { keepPreviousData, useQuery } from '@tanstack/react-query' -import { keys } from '@/api/queryKeys' -import { - ApiFileItem, - ApiFileType, - ApiFileUploadState, -} from '@/features/files/api/types.ts' -import { useUser } from '@/features/auth' -import { useConfig } from '@/api/useConfig.ts' - -type ListFilesResponse = { - count: number - next: string | null - previous: string | null - results: ApiFileItem[] -} - -type ListFilesFilters = { - is_creator_me?: boolean - type?: ApiFileType - upload_state?: ApiFileUploadState - is_deleted?: boolean -} - -export type ListFilesParams = { - filters?: ListFilesFilters - pagination: { - page: number - pageSize: number - } -} - -export const listMyFiles = async ({ - filters = {}, - pagination: { page, pageSize }, -}: ListFilesParams): Promise => { - const query = new URLSearchParams() - query.append('page', page.toString()) - query.append('page_size', pageSize.toString()) - if (filters?.is_creator_me ?? true) { - query.append('is_creator_me', 'true') - } - if (filters?.type) { - query.append('type', filters.type) - } - if (filters?.upload_state) { - query.append('upload_state', filters.upload_state) - } - if (typeof filters?.is_deleted === 'boolean') { - query.append('is_deleted', filters.is_deleted ? 'true' : 'false') - } - - return fetchApi(`/files?${query.toString()}`, { - method: 'GET', - }) -} - -export const useListMyFiles = (params: Parameters[0]) => { - const { isLoggedIn } = useUser() - const { data: appConfig } = useConfig() - return useQuery({ - queryKey: [keys.files, params], - queryFn: () => listMyFiles(params), - refetchOnMount: 'always', - placeholderData: keepPreviousData, - enabled: - isLoggedIn && appConfig?.background_image?.upload_is_enabled === true, - }) -} diff --git a/src/frontend/src/features/files/api/types.ts b/src/frontend/src/features/files/api/types.ts deleted file mode 100644 index 5c7891d6..00000000 --- a/src/frontend/src/features/files/api/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -export type ApiFileCreator = { - id: string // UUID - full_name: string | null - short_name: string | null -} - -export type ApiFileType = 'background_image' -export type ApiFileUploadState = 'pending' | 'ready' - -export type ApiFileItem = { - id: string // UUID - created_at: string // ISO datetime string - updated_at: string // ISO datetime string - title: string - type: ApiFileType - creator: ApiFileCreator - deleted_at: string | null - hard_deleted_at: string | null - filename: string - upload_state: ApiFileUploadState - mimetype: string // e.g. "image/png" - size: number // file size in bytes - description: string | null -} & ( - | { - upload_state: 'ready' - url: string - } - | { - upload_state: 'pending' - policy: string - url: null - } -) diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx index d7c5b5f1..496cce6d 100644 --- a/src/frontend/src/features/rooms/components/Conference.tsx +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -215,8 +215,8 @@ export const Conference = ({ audio={userConfig.audioEnabled} video={ userConfig.videoEnabled && { - processor: BackgroundProcessorFactory.fromProcessorConfig( - userConfig.processorConfig + processor: BackgroundProcessorFactory.deserializeProcessor( + userConfig.processorSerialized ), } } diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index 77b99785..09668b09 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -4,15 +4,15 @@ import { css } from '@/styled-system/css' import { Screen } from '@/layout/Screen' import { useEffect, useMemo, useRef, useState } from 'react' import { - createLocalAudioTrack, createLocalVideoTrack, + createLocalAudioTrack, LocalAudioTrack, LocalVideoTrack, Track, } from 'livekit-client' import { H } from '@/primitives/H' import { Field } from '@/primitives/Field' -import { Button, Dialog, Form, Text } from '@/primitives' +import { Button, Dialog, Text, Form } from '@/primitives' import { VStack } from '@/styled-system/jsx' import { Heading } from 'react-aria-components' import { RiImageCircleAiFill } from '@remixicon/react' @@ -44,7 +44,8 @@ const onError = (e: Error) => console.error('ERROR', e) const Effects = ({ videoTrack, -}: Pick) => { + onSubmit, +}: Pick) => { const { t } = useTranslation('rooms', { keyPrefix: 'join.effects' }) const [isDialogOpen, setIsDialogOpen] = useState(false) const openDialog = () => setIsDialogOpen(true) @@ -80,7 +81,7 @@ const Effects = ({ > {t('subTitle')} - + - - ) - )} - {!canUploadBackground && - uploadNotPossibleSnap.imageBackgroundConfig && ( - + {[...Array(8).keys()].map((i) => { + const imagePath = `/assets/backgrounds/${i + 1}.jpg` + const thumbnailPath = `/assets/backgrounds/thumbnails/${i + 1}.jpg` + const tooltipText = tooltipVirtualBackground(i) + return ( + { - toggleEffect( - uploadNotPossibleSnap.imageBackgroundConfig! - ) - }} - isSelected={ - deriveIdFromProcessorConfig( - uploadNotPossibleSnap.imageBackgroundConfig - ) === selectedId + aria-label={ariaLabelVirtualBackground(i, imagePath)} + isDisabled={processorPendingReveal || isDisabled} + onChange={async () => + await toggleEffect(ProcessorType.VIRTUAL, { + imagePath, + }) } + isSelected={isSelected(ProcessorType.VIRTUAL, { + imagePath, + })} className={css({ bgSize: 'cover', })} style={{ - backgroundImage: `url(${uploadNotPossibleSnap.imageBackgroundConfig.imagePath})`, + backgroundImage: `url(${thumbnailPath})`, }} - data-attr={`toggle-virtual-local`} + data-attr={`toggle-virtual-${i}`} /> - )} - { - if (e && e.item(0)) { - const file = e.item(0) as File - handleNewBackgroundFilePicked(file) - } - }} - > - - - - {!isLoggedIn && ( - - {t('virtual.personal.notLoggedInWarning')} - - )} - {!canUploadBackground && isLoggedIn && ( - - {t('virtual.personal.warningUploadDisabled')} - - )} - {hasReachedMaxNbBackgrounds && ( - - {t('virtual.personal.uploadLimitReached')} - - )} - -
- - {t('virtual.presets.title')} - -
- {processorOptions.virtualBackgrounds.map((option) => ( - - toggleEffect(option.config)} - isSelected={option.isSelected} - className={css({ - bgSize: 'cover', - })} - style={{ - backgroundImage: `url(${option.thumbnailPath})`, - }} - data-attr={`toggle-virtual-preset-${option.index}`} - /> - - ))}
@@ -911,32 +483,6 @@ export const EffectsConfiguration = ({ )} - setPersonalBackgroundHasError(false)} - onOpenChange={() => setPersonalBackgroundHasError(false)} - > -

- {t( - `virtual.personal.errors.${personalBackgroundError}.description`, - filePickerErrorContext - )} -

- - - -
) } diff --git a/src/frontend/src/features/rooms/livekit/components/effects/FunnyEffects.tsx b/src/frontend/src/features/rooms/livekit/components/effects/FunnyEffects.tsx index c2d7bba6..c19eb43a 100644 --- a/src/frontend/src/features/rooms/livekit/components/effects/FunnyEffects.tsx +++ b/src/frontend/src/features/rooms/livekit/components/effects/FunnyEffects.tsx @@ -27,7 +27,7 @@ export const FunnyEffects = ({ showFrench: false, } } - return { ...processor.options } + return processor.serialize().options } const options = getOptions() diff --git a/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts b/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts index 8edc2b82..445b38ee 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts @@ -1,7 +1,7 @@ import { useSnapshot } from 'valtio' import { userChoicesStore } from '@/stores/userChoices' import type { VideoResolution } from '@/stores/userChoices' -import { ProcessorConfig } from '@/features/rooms/livekit/components/blur' +import { ProcessorSerialized } from '@/features/rooms/livekit/components/blur' import type { VideoQuality } from 'livekit-client' export function usePersistentUserChoices() { @@ -36,8 +36,10 @@ export function usePersistentUserChoices() { saveNoiseReductionEnabled: (enabled: boolean) => { userChoicesStore.noiseReductionEnabled = enabled }, - saveProcessorConfig: (processorConfig: ProcessorConfig | undefined) => { - userChoicesStore.processorConfig = processorConfig + saveProcessorSerialized: ( + processorSerialized: ProcessorSerialized | undefined + ) => { + userChoicesStore.processorSerialized = processorSerialized }, } } diff --git a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx index 101da4a8..7a9dbedd 100644 --- a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx @@ -31,7 +31,7 @@ export const VideoTab = ({ id }: VideoTabProps) => { const { userChoices: { videoDeviceId, - processorConfig, + processorSerialized, videoPublishResolution, videoSubscribeQuality, }, @@ -78,7 +78,7 @@ export const VideoTab = ({ id }: VideoTabProps) => { resolution: VideoPresets[key].resolution, deviceId: { exact: videoDeviceId }, processor: - BackgroundProcessorFactory.fromProcessorConfig(processorConfig), + BackgroundProcessorFactory.deserializeProcessor(processorSerialized), }) } } diff --git a/src/frontend/src/features/subtitle/component/CaptionsSettings.tsx b/src/frontend/src/features/subtitle/component/CaptionsSettings.tsx index cf35d571..9f7ddefa 100644 --- a/src/frontend/src/features/subtitle/component/CaptionsSettings.tsx +++ b/src/frontend/src/features/subtitle/component/CaptionsSettings.tsx @@ -6,11 +6,8 @@ import { useSnapshot } from 'valtio' import { accessibilityStore, type CaptionTextSize, - type CaptionColor, CAPTION_TEXT_SIZE_OPTIONS, - CAPTION_COLOR_OPTIONS, } from '@/stores/accessibility' -import { useAreSubtitlesAvailable } from '../hooks/useAreSubtitlesAvailable' export const CaptionsSettings = () => { const { t } = useTranslation('settings', { @@ -27,27 +24,6 @@ export const CaptionsSettings = () => { [t] ) - const captionFontColorItems = useMemo( - () => - CAPTION_COLOR_OPTIONS.map((color) => ({ - value: color, - label: t(`fontColor.options.${color}`), - })), - [t] - ) - - const captionBackgroundColorItems = useMemo( - () => - CAPTION_COLOR_OPTIONS.map((color) => ({ - value: color, - label: t(`backgroundColor.options.${color}`), - })), - [t] - ) - - const areSubtitlesAvailable = useAreSubtitlesAvailable() - if (!areSubtitlesAvailable) return null - return (
  • { > {t('heading')} -
    - { - accessibilityStore.captionTextSize = key as CaptionTextSize - }} - wrapperProps={{ noMargin: true, fullWidth: true }} - /> - { - accessibilityStore.captionFontColor = key as CaptionColor - }} - wrapperProps={{ noMargin: true, fullWidth: true }} - /> - { - accessibilityStore.captionBackgroundColor = key as CaptionColor - }} - wrapperProps={{ noMargin: true, fullWidth: true }} - /> -
    + { + accessibilityStore.captionTextSize = key as CaptionTextSize + }} + wrapperProps={{ noMargin: true, fullWidth: true }} + />
  • ) } diff --git a/src/frontend/src/features/subtitle/component/Subtitles.tsx b/src/frontend/src/features/subtitle/component/Subtitles.tsx index f434d98d..bbc76a60 100644 --- a/src/frontend/src/features/subtitle/component/Subtitles.tsx +++ b/src/frontend/src/features/subtitle/component/Subtitles.tsx @@ -12,8 +12,6 @@ import { useSnapshot } from 'valtio' import { accessibilityStore, CAPTION_TEXT_SIZE_OPTIONS, - CAPTION_FONT_COLOR_VALUES, - CAPTION_BACKGROUND_COLOR_VALUES, type CaptionTextSize, } from '@/stores/accessibility' @@ -94,14 +92,10 @@ const useTranscriptionState = () => { } const Transcription = ({ row }: { row: TranscriptionRow }) => { - const { captionTextSize, captionFontColor, captionBackgroundColor } = - useSnapshot(accessibilityStore) + const { captionTextSize } = useSnapshot(accessibilityStore) const participantColor = getParticipantColor(row.participant) const participantName = getParticipantName(row.participant) const { fontSize, lineHeight } = CAPTION_FONT_SIZES[captionTextSize] - const fontColor = CAPTION_FONT_COLOR_VALUES[captionFontColor] - const backgroundColor = - CAPTION_BACKGROUND_COLOR_VALUES[captionBackgroundColor] const getDisplayText = (row: TranscriptionRow): string => { return row.segments @@ -134,9 +128,9 @@ const Transcription = ({ row }: { row: TranscriptionRow }) => { />
    {participantName} @@ -144,10 +138,8 @@ const Transcription = ({ row }: { row: TranscriptionRow }) => {

    {displayText}

    diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index 9aff22bf..df9821d2 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -273,40 +273,17 @@ "title": "Virtueller Hintergrund", "selectedLabel": "Hintergrund angewendet:", "apply": "Ersetze deinen Hintergrund:", - "personal": { - "title": "Meine Hintergründe", - "selectFileTooltip": "Wähle ein Bild aus, das als persönlicher Hintergrund verwendet werden soll", - "notLoggedInWarning": "Du bist nicht angemeldet, der persönliche Hintergrund wird nicht von einem Meeting zum anderen gespeichert.", - "warningUploadDisabled": "Persönliche Hintergründe werden derzeit nicht von einem Meeting zum anderen gespeichert.", - "uploadLimitReached": "Du kannst keine weiteren persönlichen Hintergründe hinzufügen.", - "uploadInProgress": "Datei wird hochgeladen…", - "errors": { - "close": "Schließen", - "file_too_large": { - "title": "Datei zu groß", - "description": "Die Datei ist zu groß. Bitte wähle eine Datei mit weniger als {{maxSize, number}} MB." - }, - "invalid_file_type": { - "title": "Ungültiger Dateityp", - "description": "Der Dateityp wird nicht unterstützt. Bitte wähle eine {{allowedExtension}}-Datei." - } - } - }, - "presets": { - "title": "Vorschläge", - "descriptions": { - "0": "Gerilltes Holzmöbel", - "1": "Besprechungsraum", - "2": "Loft mit schwarzer Glaswand", - "3": "Esszimmer", - "4": "Holzregale", - "5": "Holztreppe", - "6": "Graue Bibliothek", - "7": "Kaffeetheke" - } + "descriptions": { + "0": "Gerilltes Holzmöbel", + "1": "Besprechungsraum", + "2": "Loft mit schwarzer Glaswand", + "3": "Esszimmer", + "4": "Holzregale", + "5": "Holztreppe", + "6": "Graue Bibliothek", + "7": "Kaffeetheke" } }, - "faceLandmarks": { "title": "Visuelle Effekte", "glasses": { @@ -320,7 +297,7 @@ } }, "sidePanel": { - "ariaLabel": "Seitenleiste - {{title}}", + "ariaLabel": "Seitenleiste", "backToTools": "Zurück zu den Meeting-Tools", "heading": { "participants": "Teilnehmende", @@ -355,11 +332,11 @@ "tools": { "transcript": { "title": "Transkribieren", - "body": "Wandelt Meetings in Text um." + "body": "Das Gespräch aufzeichnen." }, "screenRecording": { - "title": "Aufnehmen", - "body": "Speichert Meetings als Video." + "title": "Aufzeichnen", + "body": "Das Meeting aufzeichnen." } } }, diff --git a/src/frontend/src/locales/de/settings.json b/src/frontend/src/locales/de/settings.json index 9879d21b..bde3f7f3 100644 --- a/src/frontend/src/locales/de/settings.json +++ b/src/frontend/src/locales/de/settings.json @@ -126,34 +126,6 @@ "medium": "Mittel", "large": "Groß" } - }, - "fontColor": { - "label": "Schriftfarbe", - "options": { - "default": "Standard", - "white": "Weiß", - "black": "Schwarz", - "blue": "Blau", - "green": "Grün", - "red": "Rot", - "yellow": "Gelb", - "cyan": "Cyan", - "magenta": "Magenta" - } - }, - "backgroundColor": { - "label": "Hintergrundfarbe", - "options": { - "default": "Standard", - "white": "Weiß", - "black": "Schwarz", - "blue": "Blau", - "green": "Grün", - "red": "Rot", - "yellow": "Gelb", - "cyan": "Cyan", - "magenta": "Magenta" - } } } }, diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index 85c3efef..034246b6 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -270,40 +270,18 @@ } }, "virtual": { - "title": "Virtual backgrounds", + "title": "Virtual background", "selectedLabel": "Background applied:", "apply": "Replace your background:", - "personal": { - "title": "My backgrounds", - "selectFileTooltip": "Select an image file to use as a personal background", - "notLoggedInWarning": "You are not logged-in, personal backgrounds won't be saved from one meeting to the other.", - "warningUploadDisabled": "Personal backgrounds are currently not saved from one meeting to the other.", - "uploadLimitReached": "You cannot upload more personal backgrounds.", - "uploadInProgress": "Image is being uploaded…", - "errors": { - "close": "Close", - "file_too_large": { - "title": "File too large", - "description": "The file is too large. Please choose a file smaller than {{maxSize, number}} MB." - }, - "invalid_file_type": { - "title": "Invalid file type", - "description": "The file type is not supported. Please choose a {{allowedExtension, list(type: 'disjunction')}} file." - } - } - }, - "presets": { - "title": "Suggestions", - "descriptions": { - "0": "Fluted wooden furniture", - "1": "Meeting room", - "2": "Loft with black glass partition", - "3": "Dining room", - "4": "Wooden shelves", - "5": "Wooden staircase", - "6": "Gray library", - "7": "Coffee counter" - } + "descriptions": { + "0": "Fluted wooden furniture", + "1": "Meeting room", + "2": "Loft with black glass partition", + "3": "Dining room", + "4": "Wooden shelves", + "5": "Wooden staircase", + "6": "Gray library", + "7": "Coffee counter" } }, "faceLandmarks": { @@ -319,7 +297,7 @@ } }, "sidePanel": { - "ariaLabel": "Sidepanel - {{title}}", + "ariaLabel": "Sidepanel", "backToTools": "Back to meeting tools", "heading": { "participants": "Participants", @@ -354,11 +332,11 @@ "tools": { "transcript": { "title": "Transcribe", - "body": "Turn meetings into text." + "body": "Record the conversation." }, "screenRecording": { "title": "Record", - "body": "Save meetings as video." + "body": "Record the meeting." } } }, diff --git a/src/frontend/src/locales/en/settings.json b/src/frontend/src/locales/en/settings.json index 722978b0..ecae6528 100644 --- a/src/frontend/src/locales/en/settings.json +++ b/src/frontend/src/locales/en/settings.json @@ -126,34 +126,6 @@ "medium": "Medium", "large": "Large" } - }, - "fontColor": { - "label": "Font color", - "options": { - "default": "Default", - "white": "White", - "black": "Black", - "blue": "Blue", - "green": "Green", - "red": "Red", - "yellow": "Yellow", - "cyan": "Cyan", - "magenta": "Magenta" - } - }, - "backgroundColor": { - "label": "Background color", - "options": { - "default": "Default", - "white": "White", - "black": "Black", - "blue": "Blue", - "green": "Green", - "red": "Red", - "yellow": "Yellow", - "cyan": "Cyan", - "magenta": "Magenta" - } } } }, diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 02f0a21f..f06afb32 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -270,40 +270,18 @@ } }, "virtual": { - "title": "Arrière-plans virtuels", + "title": "Arrière-plan virtuel", "selectedLabel": "Arrière-plan appliqué :", "apply": "Remplacer votre arrière plan :", - "personal": { - "title": "Mes arrière-plans", - "selectFileTooltip": "Sélectionnez une image à utiliser comme arrière-plan", - "notLoggedInWarning": "Vous n'êtes pas connecté, l'arrière-plan personnel ne sera pas sauvegardé d'une réunion à l'autre.", - "warningUploadDisabled": "Les arrière-plans personnels ne sont actuellement pas sauvegardés d'une réunion à l'autre.", - "uploadLimitReached": "Vous ne pouvez pas ajouter plus d'arrière-plans personnels.", - "uploadInProgress": "Image en cours d'envoi…", - "errors": { - "close": "Fermer", - "file_too_large": { - "title": "Fichier trop volumineux", - "description": "Le fichier est trop volumineux. Veuillez choisir un fichier de moins de {{maxSize, number}} Mo." - }, - "invalid_file_type": { - "title": "Type de fichier non valide", - "description": "Le type de fichier n'est pas pris en charge. Veuillez choisir un fichier {{allowedExtension}}." - } - } - }, - "presets": { - "title": "Suggestions", - "descriptions": { - "0": "Meuble cannelé en bois", - "1": "Salle de réunion", - "2": "Loft avec verrière noire", - "3": "Salle à manger", - "4": "Étagères en bois", - "5": "Escalier en bois", - "6": "Bibliothèque grise", - "7": "Comptoir de café" - } + "descriptions": { + "0": "Meuble cannelé en bois", + "1": "Salle de réunion", + "2": "Loft avec verrière noire", + "3": "Salle à manger", + "4": "Étagères en bois", + "5": "Escalier en bois", + "6": "Bibliothèque grise", + "7": "Comptoir de café" } }, "faceLandmarks": { @@ -319,7 +297,7 @@ } }, "sidePanel": { - "ariaLabel": "Panneau latéral - {{title}}", + "ariaLabel": "Panneau latéral", "backToTools": "Retour aux outils de réunion", "heading": { "participants": "Participants", @@ -354,11 +332,11 @@ "tools": { "transcript": { "title": "Transcrire", - "body": "Transcrire la réunion." + "body": "Enregistrer la conversation." }, "screenRecording": { "title": "Enregistrer", - "body": "Enregistrer la réunion en vidéo." + "body": "Enregistrer la réunion." } } }, diff --git a/src/frontend/src/locales/fr/settings.json b/src/frontend/src/locales/fr/settings.json index 99e29c82..4da44c33 100644 --- a/src/frontend/src/locales/fr/settings.json +++ b/src/frontend/src/locales/fr/settings.json @@ -126,34 +126,6 @@ "medium": "Moyen", "large": "Grand" } - }, - "fontColor": { - "label": "Couleur du texte", - "options": { - "default": "Par défaut", - "white": "Blanc", - "black": "Noir", - "blue": "Bleu", - "green": "Vert", - "red": "Rouge", - "yellow": "Jaune", - "cyan": "Cyan", - "magenta": "Magenta" - } - }, - "backgroundColor": { - "label": "Couleur de fond", - "options": { - "default": "Par défaut", - "white": "Blanc", - "black": "Noir", - "blue": "Bleu", - "green": "Vert", - "red": "Rouge", - "yellow": "Jaune", - "cyan": "Cyan", - "magenta": "Magenta" - } } } }, diff --git a/src/frontend/src/locales/nl/rooms.json b/src/frontend/src/locales/nl/rooms.json index b6364333..4979c86c 100644 --- a/src/frontend/src/locales/nl/rooms.json +++ b/src/frontend/src/locales/nl/rooms.json @@ -273,37 +273,15 @@ "title": "Virtuele achtergrond", "selectedLabel": "Achtergrond toegepast:", "apply": "Vervang je achtergrond:", - "personal": { - "title": "Mijn achtergronden", - "selectFileTooltip": "Selecteer een afbeeldingsbestand om te gebruiken als persoonlijke achtergrond", - "notLoggedInWarning": "U bent niet ingelogd, persoonlijke achtergronden worden niet opgeslagen van de ene vergadering naar de andere.", - "warningUploadDisabled": "Persoonlijke achtergronden worden momenteel niet opgeslagen van de ene vergadering naar de andere.", - "uploadLimitReached": "U kunt geen persoonlijke achtergronden meer uploaden.", - "uploadInProgress": "Afbeelding wordt geüpload…", - "errors": { - "close": "Sluiten", - "file_too_large": { - "title": "Bestand te groot", - "description": "Het bestand is te groot. Kies een bestand kleiner dan {{maxSize, number}} MB." - }, - "invalid_file_type": { - "title": "Ongeldig bestandstype", - "description": "Het bestandstype wordt niet ondersteund. Kies een {{allowedExtension, list(type: 'disjunction')}} bestand." - } - } - }, - "presets": { - "title": "Suggesties", - "descriptions": { - "0": "Geprofileerd houten meubel", - "1": "Vergaderruimte", - "2": "Loft met zwarte glaswand", - "3": "Eetkamer", - "4": "Houten planken", - "5": "Houten trap", - "6": "Grijze bibliotheek", - "7": "Koffiebar" - } + "descriptions": { + "0": "Geprofileerd houten meubel", + "1": "Vergaderruimte", + "2": "Loft met zwarte glaswand", + "3": "Eetkamer", + "4": "Houten planken", + "5": "Houten trap", + "6": "Grijze bibliotheek", + "7": "Koffiebar" } }, "faceLandmarks": { @@ -319,7 +297,7 @@ } }, "sidePanel": { - "ariaLabel": "Zijbalk - {{title}}", + "ariaLabel": "Zijbalk", "backToTools": "Terug naar vergadertools", "heading": { "participants": "Deelnemers", @@ -354,11 +332,11 @@ "tools": { "transcript": { "title": "Transcriberen", - "body": "Zet vergaderingen om in tekst." + "body": "Het gesprek opnemen." }, "screenRecording": { "title": "Opnemen", - "body": "Sla vergaderingen op als video." + "body": "De vergadering opnemen." } } }, diff --git a/src/frontend/src/locales/nl/settings.json b/src/frontend/src/locales/nl/settings.json index ec971879..587f9597 100644 --- a/src/frontend/src/locales/nl/settings.json +++ b/src/frontend/src/locales/nl/settings.json @@ -126,34 +126,6 @@ "medium": "Gemiddeld", "large": "Groot" } - }, - "fontColor": { - "label": "Tekstkleur", - "options": { - "default": "Standaard", - "white": "Wit", - "black": "Zwart", - "blue": "Blauw", - "green": "Groen", - "red": "Rood", - "yellow": "Geel", - "cyan": "Cyaan", - "magenta": "Magenta" - } - }, - "backgroundColor": { - "label": "Achtergrondkleur", - "options": { - "default": "Standaard", - "white": "Wit", - "black": "Zwart", - "blue": "Blauw", - "green": "Groen", - "red": "Rood", - "yellow": "Geel", - "cyan": "Cyaan", - "magenta": "Magenta" - } } } }, diff --git a/src/frontend/src/primitives/Spinner.tsx b/src/frontend/src/primitives/Spinner.tsx index 3224f442..d921d11a 100644 --- a/src/frontend/src/primitives/Spinner.tsx +++ b/src/frontend/src/primitives/Spinner.tsx @@ -20,11 +20,7 @@ export const Spinner = ({ const r = 14 - strokeWidth const c = 2 * r * Math.PI return ( - + {({ percentage }) => (
    = { - default: '#FFFFFF', - white: '#FFFFFF', - black: '#000000', - blue: '#0000FF', - green: '#00FF00', - red: '#FF0000', - yellow: '#FFFF00', - cyan: '#00FFFF', - magenta: '#FF00FF', -} - -export const CAPTION_BACKGROUND_COLOR_VALUES: Record = { - default: 'rgba(0, 0, 0, 0.75)', - black: 'rgba(0, 0, 0, 0.75)', - white: 'rgba(255, 255, 255, 0.75)', - blue: 'rgba(0, 0, 255, 0.75)', - green: 'rgba(0, 255, 0, 0.75)', - red: 'rgba(255, 0, 0, 0.75)', - yellow: 'rgba(255, 255, 0, 0.75)', - cyan: 'rgba(0, 255, 255, 0.75)', - magenta: 'rgba(255, 0, 255, 0.75)', -} - type AccessibilityState = { announceReactions: boolean captionTextSize: CaptionTextSize - captionFontColor: CaptionColor - captionBackgroundColor: CaptionColor } const DEFAULT_STATE: AccessibilityState = { announceReactions: false, captionTextSize: 'medium', - captionFontColor: 'default', - captionBackgroundColor: 'default', } function getAccessibilityState(): AccessibilityState { @@ -76,21 +25,10 @@ function getAccessibilityState(): AccessibilityState { const stored = localStorage.getItem(STORAGE_KEYS.ACCESSIBILITY) if (stored) { const parsed = JSON.parse(stored) - const captionTextSize = CAPTION_TEXT_SIZE_OPTIONS.includes( - parsed.captionTextSize - ) + const validCaptionSizes = CAPTION_TEXT_SIZE_OPTIONS + const captionTextSize = validCaptionSizes.includes(parsed.captionTextSize) ? parsed.captionTextSize : DEFAULT_STATE.captionTextSize - const captionFontColor = CAPTION_COLOR_OPTIONS.includes( - parsed.captionFontColor - ) - ? parsed.captionFontColor - : DEFAULT_STATE.captionFontColor - const captionBackgroundColor = CAPTION_COLOR_OPTIONS.includes( - parsed.captionBackgroundColor - ) - ? parsed.captionBackgroundColor - : DEFAULT_STATE.captionBackgroundColor return { ...DEFAULT_STATE, ...parsed, @@ -99,8 +37,6 @@ function getAccessibilityState(): AccessibilityState { ? parsed.announceReactions : DEFAULT_STATE.announceReactions, captionTextSize, - captionFontColor, - captionBackgroundColor, } } diff --git a/src/frontend/src/stores/userChoices.ts b/src/frontend/src/stores/userChoices.ts index 0d3e89d5..345838cb 100644 --- a/src/frontend/src/stores/userChoices.ts +++ b/src/frontend/src/stores/userChoices.ts @@ -1,19 +1,16 @@ import { proxy, subscribe } from 'valtio' -import { - ProcessorConfig, - ProcessorType, -} from '@/features/rooms/livekit/components/blur' +import { ProcessorSerialized } from '@/features/rooms/livekit/components/blur' import { loadUserChoices, - LocalUserChoices as LocalUserChoicesLK, saveUserChoices, + LocalUserChoices as LocalUserChoicesLK, } from '@livekit/components-core' import { VideoQuality } from 'livekit-client' export type VideoResolution = 'h720' | 'h360' | 'h180' export type LocalUserChoices = LocalUserChoicesLK & { - processorConfig?: ProcessorConfig + processorSerialized?: ProcessorSerialized noiseReductionEnabled?: boolean audioOutputDeviceId?: string videoPublishResolution?: VideoResolution @@ -31,33 +28,7 @@ function getUserChoicesState(): LocalUserChoices { } export const userChoicesStore = proxy(getUserChoicesState()) + subscribe(userChoicesStore, () => { saveUserChoices(userChoicesStore, false) }) - -// we run some logic on store loading to check if the processor config is still valid -if (userChoicesStore.processorConfig?.type === ProcessorType.VIRTUAL) { - if (userChoicesStore.processorConfig.imagePath.startsWith('blob:')) { - // this happens when a not authenticated user had changed their background image - // we restore clear the processor config to avoid displaying a black screen. - userChoicesStore.processorConfig = undefined - } else if (userChoicesStore.processorConfig.fileId) { - // Checking if the image is still available / accessible - await fetch(userChoicesStore.processorConfig.imagePath, { - // We bypass the cache to ensure we have access - cache: 'reload', - }) - .then((response) => { - // if we cannot fetch the image (likely a 401 from the backend because - // the user is not logged in anymore, etc.), - // we clear the processor config to avoid displaying a black screen. - // This can happen when the user logs out for instance, etc. - if (!response.ok) { - userChoicesStore.processorConfig = undefined - } - }) - .catch(() => { - userChoicesStore.processorConfig = undefined - }) - } -} diff --git a/src/frontend/src/styles/index.css b/src/frontend/src/styles/index.css index 39504da3..900d7a3a 100644 --- a/src/frontend/src/styles/index.css +++ b/src/frontend/src/styles/index.css @@ -95,14 +95,3 @@ html:has(.lk-video-conference) { U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } - -.hoverGroup .hoverGroupChild { - opacity: 0; - pointer-events: none; -} - -.hoverGroup:hover .hoverGroupChild, -.hoverGroup:focus-within .hoverGroupChild { - opacity: 1; - pointer-events: auto; -} diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts index 0887b027..fd6b5d5c 100644 --- a/src/frontend/vite.config.ts +++ b/src/frontend/vite.config.ts @@ -14,14 +14,6 @@ export default defineConfig(({ mode }) => { port: parseInt(env.VITE_PORT) || 3000, host: env.VITE_HOST ?? 'localhost', allowedHosts: ['.nip.io'], - // In a local dev setup, we proxy the media server ourselves to avoid CORS issues - proxy: { - '/media': { - target: 'http://localhost:8083', - changeOrigin: true, - secure: false - } - } }, } }) diff --git a/src/mail/mjml/screen_recording.mjml b/src/mail/mjml/screen_recording.mjml index 24d23eb5..75c69933 100644 --- a/src/mail/mjml/screen_recording.mjml +++ b/src/mail/mjml/screen_recording.mjml @@ -9,7 +9,7 @@ align="center" src="{{logo_img}}" width="320px" - alt="{%trans 'Logo email' %} {{brandname}}" + alt="{%trans 'Logo email' %}" /> diff --git a/src/mail/package-lock.json b/src/mail/package-lock.json index bd1ffa19..09afbbd9 100644 --- a/src/mail/package-lock.json +++ b/src/mail/package-lock.json @@ -1,12 +1,12 @@ { "name": "mail_mjml", - "version": "1.11.0", + "version": "1.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mail_mjml", - "version": "1.11.0", + "version": "1.10.0", "license": "MIT", "dependencies": { "@html-to/text-cli": "0.5.4", diff --git a/src/mail/package.json b/src/mail/package.json index 5fa64419..be6cb5ef 100644 --- a/src/mail/package.json +++ b/src/mail/package.json @@ -1,6 +1,6 @@ { "name": "mail_mjml", - "version": "1.11.0", + "version": "1.10.0", "description": "An util to generate html and text django's templates from mjml templates", "type": "module", "dependencies": { diff --git a/src/sdk/package-lock.json b/src/sdk/package-lock.json index ff2db31b..26f82f73 100644 --- a/src/sdk/package-lock.json +++ b/src/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "sdk", - "version": "1.11.0", + "version": "1.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sdk", - "version": "1.11.0", + "version": "1.10.0", "license": "ISC", "workspaces": [ "./library", diff --git a/src/sdk/package.json b/src/sdk/package.json index 8806f18f..e6a42814 100644 --- a/src/sdk/package.json +++ b/src/sdk/package.json @@ -1,6 +1,6 @@ { "name": "sdk", - "version": "1.11.0", + "version": "1.10.0", "author": "", "license": "ISC", "description": "", diff --git a/src/summary/pyproject.toml b/src/summary/pyproject.toml index 39a3ce1d..bc300add 100644 --- a/src/summary/pyproject.toml +++ b/src/summary/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "summary" -version = "1.11.0" +version = "1.10.0" dependencies = [ "fastapi[standard]>=0.105.0", "uvicorn>=0.24.0", @@ -21,16 +21,8 @@ dependencies = [ [project.optional-dependencies] dev = [ "ruff==0.14.4", - "pytest==9.0.2", - "responses>=0.25.8", ] -[tool.pytest.ini_options] -markers = [ - "api: Test the API", -] -testpaths = ["tests"] - [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" @@ -57,7 +49,6 @@ select = [ "T20", # flake8-print "W", # pycodestyle warning ] -ignore= ["PLR2004"] [tool.ruff.lint.per-file-ignores] "tests/*" = [ diff --git a/src/summary/summary/core/locales/de.py b/src/summary/summary/core/locales/de.py index 9d13f413..ae792b6a 100644 --- a/src/summary/summary/core/locales/de.py +++ b/src/summary/summary/core/locales/de.py @@ -23,7 +23,8 @@ Einige Punkte, die wir Ihnen empfehlen zu überprüfen: """, download_header_template=( - "\n*[Laden Sie hier Ihre Aufnahme herunter (externer Link)]({download_link})*\n" + "\n*Laden Sie Ihre Aufnahme herunter, " + "indem Sie [diesem Link folgen]({download_link})*\n" ), hallucination_replacement_text="[Text konnte nicht transkribiert werden]", document_default_title="Transkription", diff --git a/src/summary/summary/core/locales/en.py b/src/summary/summary/core/locales/en.py index a70ef1a9..6534e924 100644 --- a/src/summary/summary/core/locales/en.py +++ b/src/summary/summary/core/locales/en.py @@ -23,7 +23,7 @@ A few things we recommend you check: """, download_header_template=( - "\n*[Download your recording (external link)]({download_link})*\n" + "\n*Download your recording by [following this link]({download_link})*\n" ), hallucination_replacement_text="[Unable to transcribe text]", document_default_title="Transcription", diff --git a/src/summary/summary/core/locales/fr.py b/src/summary/summary/core/locales/fr.py index 584c2036..48a1f00e 100644 --- a/src/summary/summary/core/locales/fr.py +++ b/src/summary/summary/core/locales/fr.py @@ -23,7 +23,7 @@ Quelques points que nous vous conseillons de vérifier : """, download_header_template=( - "\n*[Télécharger votre enregistrement (lien externe)]({download_link})*\n" + "\n*Télécharger votre enregistrement en [suivant ce lien]({download_link})*\n" ), hallucination_replacement_text="[Texte impossible à transcrire]", document_default_title="Transcription", diff --git a/src/summary/summary/core/locales/nl.py b/src/summary/summary/core/locales/nl.py index d57e6cd5..0cb00213 100644 --- a/src/summary/summary/core/locales/nl.py +++ b/src/summary/summary/core/locales/nl.py @@ -23,7 +23,7 @@ Een paar punten die wij u aanraden te controleren: """, download_header_template=( - "\n*[Download hier je opname (externe link)]({download_link})*\n" + "\n*Download uw opname door [deze link te volgen]({download_link})*\n" ), hallucination_replacement_text="[Tekst kon niet worden getranscribeerd]", document_default_title="Transcriptie", diff --git a/src/summary/tests/__init__.py b/src/summary/tests/__init__.py deleted file mode 100644 index 224e5ad5..00000000 --- a/src/summary/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the summary service.""" diff --git a/src/summary/tests/api/__init__.py b/src/summary/tests/api/__init__.py deleted file mode 100644 index bce0b500..00000000 --- a/src/summary/tests/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the API summary service.""" diff --git a/src/summary/tests/api/test_api_health.py b/src/summary/tests/api/test_api_health.py deleted file mode 100644 index 0460b1d8..00000000 --- a/src/summary/tests/api/test_api_health.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Integration tests for the health check endpoints.""" - - -class TestHeartbeat: - """Tests for the /__heartbeat__ endpoint.""" - - def test_returns_200(self, client): - """The heartbeat endpoint responds with 200 OK without a token.""" - response = client.get("/__heartbeat__") - - assert response.status_code == 200 - - -class TestLBHeartbeat: - """Tests for the /__lbheartbeat__ endpoint.""" - - def test_returns_200(self, client): - """The load-balancer heartbeat endpoint responds with 200 OK without a token.""" - response = client.get("/__lbheartbeat__") - - assert response.status_code == 200 diff --git a/src/summary/tests/api/test_api_tasks.py b/src/summary/tests/api/test_api_tasks.py deleted file mode 100644 index 9492bf95..00000000 --- a/src/summary/tests/api/test_api_tasks.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Integration tests for the API tasks endpoints.""" - -# tests/unit/test_api_tasks.py -from unittest.mock import MagicMock, patch - - -class TestTasks: - """Tests for the /tasks endpoint.""" - - @patch( - "summary.api.route.tasks.process_audio_transcribe_summarize_v2.apply_async", - return_value=MagicMock(id="task-id-abc"), - ) - @patch("summary.api.route.tasks.time.time", return_value=1735725600.0) - def test_create_task_returns_task_id(self, mock_time, mock_apply_async, client): - """POST /tasks/ with valid payload returns id and dispatches Celery task.""" - response = client.post( - "api/v1/tasks/", - headers={"Authorization": "Bearer test-api-token"}, - json={ - "owner_id": "owner-123", - "filename": "recording.mp4", - "email": "user@example.com", - "sub": "sub-123", - "room": "room-abc", - "recording_date": "2026-01-01", - "recording_time": "10:00:00", - "language": None, - "download_link": "http://example.com/file.mp4", - }, - ) - - assert response.status_code == 200 - assert response.json() == {"id": "task-id-abc", "message": "Task created"} - - args = mock_apply_async.call_args.kwargs["args"] - assert args == [ - "owner-123", # owner_id - "recording.mp4", # filename - "user@example.com", # email - "sub-123", # sub - 1735725600.0, # frozen time - "room-abc", # room - "2026-01-01", # recording_date - "10:00:00", # recording_time - None, # language - "http://example.com/file.mp4", # download_link - None, # context_language - ] - - def test_create_task_invalid_language(self, client): - """POST /tasks/ with an unsupported language returns 422.""" - payload = {"language": "klingon"} - response = client.post( - "/api/v1/tasks/", - headers={"Authorization": "Bearer test-api-token"}, - json=payload, - ) - - assert response.status_code == 422 - - @patch( - "summary.api.route.tasks.AsyncResult", - return_value=MagicMock(status="PENDING"), - ) - def test_get_task_status_pending(self, mock_result, client): - """GET /tasks/{id} returns PENDING status when the task has not started yet.""" - response = client.get( - "/api/v1/tasks/task-id-abc", - headers={"Authorization": "Bearer test-api-token"}, - ) - - assert response.status_code == 200 - assert response.json() == {"id": "task-id-abc", "status": "PENDING"} - - @patch( - "summary.api.route.tasks.AsyncResult", - return_value=MagicMock(status="SUCCESS"), - ) - def test_get_task_status_success(self, mock_result, client): - """GET /tasks/{id} returns SUCCESS status when the task has completed.""" - response = client.get( - "/api/v1/tasks/task-id-abc", - headers={"Authorization": "Bearer test-api-token"}, - ) - - assert response.status_code == 200 - assert response.json()["status"] == "SUCCESS" diff --git a/src/summary/tests/conftest.py b/src/summary/tests/conftest.py deleted file mode 100644 index e002c46e..00000000 --- a/src/summary/tests/conftest.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Integration test configuration. Provides shared fixtures.""" - -import pytest -from fastapi.testclient import TestClient -from pydantic import SecretStr - -from summary.core.config import Settings, get_settings -from summary.main import app - - -def get_settings_override(): - """Return settings for tests.""" - return Settings( - app_api_token=SecretStr("test-api-token"), - ) - - -@pytest.fixture() -def client(): - """Provide a FastAPI TestClient for tests.""" - client = TestClient(app) - app.dependency_overrides[get_settings] = get_settings_override - - return client