mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-30 14:38:33 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a8906c0a1 | |||
| 99ba8e330e | |||
| 059e5f1ec4 | |||
| 39ab9359e4 |
@@ -8,6 +8,16 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- 🔒(backend) throttle meeting link generation
|
||||
- 🔒️(backend) add a daily cap on room creation
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) enforce recording-mode permissions on the checkboxes
|
||||
- 🔒️(agents) fix util-linux CVEs reported by Cyberwatch
|
||||
|
||||
## [1.33.0] - 2026-09-30
|
||||
|
||||
### Added
|
||||
|
||||
+119
-117
@@ -14,7 +14,7 @@ This document is a step-by-step guide that describes how to install LaSuite Meet
|
||||
|
||||
If you do not have a kubernetes test cluster, you can install everything on a local kind cluster. In this case, the simplest way is to use our script located in this repo under **bin/start-kind.sh**.
|
||||
|
||||
IMPORTANT: The kind method will only deploy meet as a local instance(127.0.0.1) that can only be accessed from the device where it has been deployed.
|
||||
IMPORTANT: The kind method will only deploy meet as a local instance(127.0.0.1) that can only be accessed from the device where it has been deployed.
|
||||
|
||||
To be able to use the script, you will need to install the following components:
|
||||
|
||||
@@ -311,120 +311,122 @@ frontend:
|
||||
|
||||
These are the environmental options available on meet backend.
|
||||
|
||||
| Option | Description | default |
|
||||
|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| DATA_DIR | Data directory location | /data |
|
||||
| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] |
|
||||
| DJANGO_SECRET_KEY | Secret key used for Django security | |
|
||||
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] |
|
||||
| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false |
|
||||
| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 |
|
||||
| DB_NAME | Name of the database | meet |
|
||||
| DB_USER | User used to connect to database | dinum |
|
||||
| DB_PASSWORD | Password used to connect to the database | pass |
|
||||
| DB_HOST | Hostname of the database | localhost |
|
||||
| DB_PORT | Port to connect to database | 5432 |
|
||||
| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
||||
| AWS_S3_ENDPOINT_URL | S3 host endpoint | |
|
||||
| AWS_S3_ACCESS_KEY_ID | S3 access key | |
|
||||
| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | |
|
||||
| AWS_S3_REGION_NAME | S3 region | |
|
||||
| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage |
|
||||
| DJANGO_LANGUAGE_CODE | Default language | en-us |
|
||||
| REDIS_URL | Redis endpoint | redis://redis:6379/1 |
|
||||
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
|
||||
| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute |
|
||||
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
|
||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
|
||||
| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] |
|
||||
| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `<link>` tag with this URL as href is added to the `<head>` of the frontend app | |
|
||||
| FRONTEND_ANALYTICS | Analytics information | {} |
|
||||
| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} |
|
||||
| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} |
|
||||
| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false |
|
||||
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
|
||||
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
|
||||
| FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | |
|
||||
| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false |
|
||||
| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false |
|
||||
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||
| DJANGO_EMAIL_HOST | Host of the email server | |
|
||||
| DJANGO_EMAIL_HOST_USER | User to connect to the email server | |
|
||||
| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | |
|
||||
| DJANGO_EMAIL_PORT | Port to connect to the email server | |
|
||||
| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false |
|
||||
| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false |
|
||||
| DJANGO_EMAIL_FROM | Email from account | from@example.com |
|
||||
| EMAIL_BRAND_NAME | Email branding name | |
|
||||
| EMAIL_SUPPORT_EMAIL | Support email address | |
|
||||
| EMAIL_LOGO_IMG | Email logo image | |
|
||||
| EMAIL_DOMAIN | Email domain | |
|
||||
| EMAIL_APP_BASE_URL | Email app base URL | |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] |
|
||||
| SENTRY_DSN | Sentry server DSN | |
|
||||
| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 |
|
||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} |
|
||||
| OIDC_CREATE_USER | Create OIDC user if not exists | true |
|
||||
| OIDC_VERIFY_SSL | Verify SSL for OIDC | true |
|
||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false |
|
||||
| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 |
|
||||
| OIDC_RP_CLIENT_ID | OIDC client ID | meet |
|
||||
| OIDC_RP_CLIENT_SECRET | OIDC client secret | |
|
||||
| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | |
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | |
|
||||
| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | |
|
||||
| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} |
|
||||
| OIDC_RP_SCOPES | OIDC scopes | openid email |
|
||||
| OIDC_USE_NONCE | Use nonce for OIDC | true |
|
||||
| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false |
|
||||
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] |
|
||||
| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true |
|
||||
| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name |
|
||||
| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] |
|
||||
| OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False |
|
||||
| OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 |
|
||||
| OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 |
|
||||
| LOGIN_REDIRECT_URL | Login redirect URL | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | |
|
||||
| LOGOUT_REDIRECT_URL | URL to redirect to on logout | |
|
||||
| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true |
|
||||
| LIVEKIT_API_KEY | LiveKit API key | |
|
||||
| LIVEKIT_API_SECRET | LiveKit API secret | |
|
||||
| LIVEKIT_API_URL | LiveKit API URL | |
|
||||
| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true |
|
||||
| LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false |
|
||||
| LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false |
|
||||
| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public |
|
||||
| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true |
|
||||
| ROOM_INACTIVITY_DELETION_DAYS | Days without being started after which a room is purged. Unset to never purge | |
|
||||
| RECORDING_ENABLE | Record meeting option | false |
|
||||
| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings |
|
||||
| Option | Description | default |
|
||||
|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| DATA_DIR | Data directory location | /data |
|
||||
| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] |
|
||||
| DJANGO_SECRET_KEY | Secret key used for Django security | |
|
||||
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] |
|
||||
| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false |
|
||||
| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 |
|
||||
| DB_NAME | Name of the database | meet |
|
||||
| DB_USER | User used to connect to database | dinum |
|
||||
| DB_PASSWORD | Password used to connect to the database | pass |
|
||||
| DB_HOST | Hostname of the database | localhost |
|
||||
| DB_PORT | Port to connect to database | 5432 |
|
||||
| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
||||
| AWS_S3_ENDPOINT_URL | S3 host endpoint | |
|
||||
| AWS_S3_ACCESS_KEY_ID | S3 access key | |
|
||||
| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | |
|
||||
| AWS_S3_REGION_NAME | S3 region | |
|
||||
| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage |
|
||||
| DJANGO_LANGUAGE_CODE | Default language | en-us |
|
||||
| REDIS_URL | Redis endpoint | redis://redis:6379/1 |
|
||||
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
|
||||
| ROOM_CREATION_THROTTLE_RATES | Room creation throttle rate per authenticated user | 50/minute | 50/minute |
|
||||
| ROOM_CREATION_DAILY_THROTTLE_RATES | Daily room creation cap per authenticated user | 1000/day |
|
||||
| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute |
|
||||
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
|
||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
|
||||
| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] |
|
||||
| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `<link>` tag with this URL as href is added to the `<head>` of the frontend app | |
|
||||
| FRONTEND_ANALYTICS | Analytics information | {} |
|
||||
| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} |
|
||||
| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} |
|
||||
| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false |
|
||||
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
|
||||
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
|
||||
| FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | |
|
||||
| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false |
|
||||
| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false |
|
||||
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||
| DJANGO_EMAIL_HOST | Host of the email server | |
|
||||
| DJANGO_EMAIL_HOST_USER | User to connect to the email server | |
|
||||
| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | |
|
||||
| DJANGO_EMAIL_PORT | Port to connect to the email server | |
|
||||
| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false |
|
||||
| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false |
|
||||
| DJANGO_EMAIL_FROM | Email from account | from@example.com |
|
||||
| EMAIL_BRAND_NAME | Email branding name | |
|
||||
| EMAIL_SUPPORT_EMAIL | Support email address | |
|
||||
| EMAIL_LOGO_IMG | Email logo image | |
|
||||
| EMAIL_DOMAIN | Email domain | |
|
||||
| EMAIL_APP_BASE_URL | Email app base URL | |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] |
|
||||
| SENTRY_DSN | Sentry server DSN | |
|
||||
| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 |
|
||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} |
|
||||
| OIDC_CREATE_USER | Create OIDC user if not exists | true |
|
||||
| OIDC_VERIFY_SSL | Verify SSL for OIDC | true |
|
||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false |
|
||||
| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 |
|
||||
| OIDC_RP_CLIENT_ID | OIDC client ID | meet |
|
||||
| OIDC_RP_CLIENT_SECRET | OIDC client secret | |
|
||||
| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | |
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | |
|
||||
| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | |
|
||||
| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} |
|
||||
| OIDC_RP_SCOPES | OIDC scopes | openid email |
|
||||
| OIDC_USE_NONCE | Use nonce for OIDC | true |
|
||||
| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false |
|
||||
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] |
|
||||
| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true |
|
||||
| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name |
|
||||
| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] |
|
||||
| OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False |
|
||||
| OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 |
|
||||
| OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 |
|
||||
| LOGIN_REDIRECT_URL | Login redirect URL | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | |
|
||||
| LOGOUT_REDIRECT_URL | URL to redirect to on logout | |
|
||||
| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true |
|
||||
| LIVEKIT_API_KEY | LiveKit API key | |
|
||||
| LIVEKIT_API_SECRET | LiveKit API secret | |
|
||||
| LIVEKIT_API_URL | LiveKit API URL | |
|
||||
| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true |
|
||||
| LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false |
|
||||
| LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false |
|
||||
| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public |
|
||||
| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true |
|
||||
| ROOM_INACTIVITY_DELETION_DAYS | Days without being started after which a room is purged. Unset to never purge | |
|
||||
| RECORDING_ENABLE | Record meeting option | false |
|
||||
| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings |
|
||||
| RECORDING_WORKER_CLASSES | Worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} |
|
||||
| RECORDING_EXPIRATION_DAYS | Recording expiration in days | |
|
||||
| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | |
|
||||
| SCREEN_RECORDING_BASE_URL | Screen recording base URL | |
|
||||
| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | |
|
||||
| SUMMARY_SERVICE_API_TOKEN | API token for summary service | |
|
||||
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false |
|
||||
| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService |
|
||||
| BREVO_API_KEY | Brevo API key for marketing emails | |
|
||||
| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] |
|
||||
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} |
|
||||
| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 |
|
||||
| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby |
|
||||
| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 |
|
||||
| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 |
|
||||
| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) |
|
||||
| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting |
|
||||
| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId |
|
||||
| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) |
|
||||
| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false |
|
||||
| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 |
|
||||
| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 |
|
||||
| RECORDING_EXPIRATION_DAYS | Recording expiration in days | |
|
||||
| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | |
|
||||
| SCREEN_RECORDING_BASE_URL | Screen recording base URL | |
|
||||
| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | |
|
||||
| SUMMARY_SERVICE_API_TOKEN | API token for summary service | |
|
||||
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false |
|
||||
| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService |
|
||||
| BREVO_API_KEY | Brevo API key for marketing emails | |
|
||||
| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] |
|
||||
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} |
|
||||
| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 |
|
||||
| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby |
|
||||
| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 |
|
||||
| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 |
|
||||
| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) |
|
||||
| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting |
|
||||
| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId |
|
||||
| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) |
|
||||
| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false |
|
||||
| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 |
|
||||
| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.14.6-slim AS base
|
||||
FROM python:3.14.7-slim AS base
|
||||
|
||||
# Install system dependencies required by LiveKit, fetching packages over HTTPS only for Bureautix proxy
|
||||
RUN sed -i "s|^URIs: http://|URIs: https://|" /etc/apt/sources.list.d/debian.sources \
|
||||
|
||||
@@ -20,6 +20,33 @@ class MonitoredUserRateThrottle(MonitoredThrottleMixin, UserRateThrottle):
|
||||
"""Throttle for the monitored scoped rate throttle."""
|
||||
|
||||
|
||||
class RoomCreationUserRateThrottle(MonitoredUserRateThrottle):
|
||||
"""Throttle room creation per authenticated user.
|
||||
|
||||
Can be declared at the viewset level: every action other than "create"
|
||||
is left unthrottled, so the same class can be reused on any viewset
|
||||
exposing a room creation endpoint.
|
||||
"""
|
||||
|
||||
scope = "room_creation"
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
"""Throttle only room creations."""
|
||||
if getattr(view, "action", None) != "create":
|
||||
return None
|
||||
return super().get_cache_key(request, view)
|
||||
|
||||
|
||||
class RoomCreationDailyUserRateThrottle(RoomCreationUserRateThrottle):
|
||||
"""Cap room creation per authenticated user over a day.
|
||||
|
||||
Complements the short-term RoomCreationUserRateThrottle, which absorbs
|
||||
bursts but lets a user steadily create rooms over hours or days.
|
||||
"""
|
||||
|
||||
scope = "room_creation_daily"
|
||||
|
||||
|
||||
class RequestEntryAuthenticatedUserRateThrottle(MonitoredUserRateThrottle):
|
||||
"""Throttle authenticated user requesting room entry"""
|
||||
|
||||
|
||||
@@ -180,6 +180,10 @@ class RoomViewSet(
|
||||
permission_classes = [permissions.RoomPermissions]
|
||||
queryset = models.Room.objects.all()
|
||||
serializer_class = serializers.RoomSerializer
|
||||
throttle_classes = [
|
||||
throttling.RoomCreationUserRateThrottle,
|
||||
throttling.RoomCreationDailyUserRateThrottle,
|
||||
]
|
||||
|
||||
def get_object(self):
|
||||
"""Allow getting a room by its slug."""
|
||||
|
||||
@@ -9,6 +9,10 @@ from django.core.cache import cache
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from ...api.throttling import (
|
||||
RoomCreationDailyUserRateThrottle,
|
||||
RoomCreationUserRateThrottle,
|
||||
)
|
||||
from ...factories import RoomFactory, UserFactory
|
||||
from ...models import Room, RoomAccessLevel
|
||||
|
||||
@@ -312,3 +316,145 @@ def test_api_rooms_create_authenticated_blank_user_default_access_level():
|
||||
assert response.status_code == 201
|
||||
room = Room.objects.get()
|
||||
assert room.access_level == settings.RESOURCE_DEFAULT_ACCESS_LEVEL
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def room_creation_throttle(monkeypatch):
|
||||
"""Lower the room creation rate for the duration of a test."""
|
||||
monkeypatch.setitem(
|
||||
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"], "room_creation", "2/minute"
|
||||
)
|
||||
|
||||
|
||||
def test_api_rooms_create_throttled(room_creation_throttle):
|
||||
"""Excess requests are rejected and create no room."""
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(UserFactory())
|
||||
|
||||
for index in range(2):
|
||||
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
|
||||
assert response.status_code == 201
|
||||
|
||||
response = client.post("/api/v1.0/rooms/", {"name": "Blocked room"})
|
||||
assert response.status_code == 429
|
||||
assert 0 < int(response["Retry-After"]) <= 60
|
||||
assert Room.objects.count() == 2
|
||||
|
||||
|
||||
def test_api_rooms_create_throttle_per_user(room_creation_throttle):
|
||||
"""Users sharing an IP have independent creation limits."""
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(UserFactory())
|
||||
for index in range(2):
|
||||
response = client.post("/api/v1.0/rooms/", {"name": f"First user room {index}"})
|
||||
assert response.status_code == 201
|
||||
|
||||
response = client.post("/api/v1.0/rooms/", {"name": "Blocked room"})
|
||||
assert response.status_code == 429
|
||||
|
||||
client.force_login(UserFactory())
|
||||
response = client.post("/api/v1.0/rooms/", {"name": "Second user room"})
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
def test_api_rooms_create_throttle_does_not_limit_other_actions(room_creation_throttle):
|
||||
"""Exhausting creation capacity leaves listing and updating available."""
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(UserFactory())
|
||||
for index in range(2):
|
||||
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
|
||||
assert response.status_code == 201
|
||||
room_id = response.json()["id"]
|
||||
|
||||
assert client.post("/api/v1.0/rooms/", {"name": "Blocked room"}).status_code == 429
|
||||
assert client.get("/api/v1.0/rooms/").status_code == 200
|
||||
assert (
|
||||
client.patch(
|
||||
f"/api/v1.0/rooms/{room_id}/", {"name": "Renamed room"}
|
||||
).status_code
|
||||
== 200
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def daily_room_creation_throttle(monkeypatch):
|
||||
"""Use a tiny daily cap, a loose burst limit and a controllable clock.
|
||||
|
||||
Rates are patched with monkeypatch.setitem so they are restored after the
|
||||
test. Returns a one-item list holding the current fake timestamp.
|
||||
"""
|
||||
rates = RoomCreationDailyUserRateThrottle.THROTTLE_RATES
|
||||
monkeypatch.setitem(rates, "room_creation", "100/minute")
|
||||
monkeypatch.setitem(rates, "room_creation_daily", "3/day")
|
||||
now = [1_000_000.0]
|
||||
monkeypatch.setattr(RoomCreationUserRateThrottle, "timer", lambda self: now[0])
|
||||
return now
|
||||
|
||||
|
||||
def test_api_rooms_create_daily_throttled(daily_room_creation_throttle):
|
||||
"""The daily cap still applies once the short-term window has elapsed."""
|
||||
now = daily_room_creation_throttle
|
||||
client = APIClient()
|
||||
client.force_login(UserFactory())
|
||||
|
||||
for index in range(3):
|
||||
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
|
||||
assert response.status_code == 201
|
||||
now[0] += 120 # Spread creations beyond the short-term window.
|
||||
|
||||
response = client.post("/api/v1.0/rooms/", {"name": "Blocked room"})
|
||||
assert response.status_code == 429
|
||||
assert int(response["Retry-After"]) > 60
|
||||
assert Room.objects.count() == 3
|
||||
|
||||
|
||||
def test_api_rooms_create_daily_throttle_resets(daily_room_creation_throttle):
|
||||
"""Room creation is allowed again once a day has passed."""
|
||||
now = daily_room_creation_throttle
|
||||
client = APIClient()
|
||||
client.force_login(UserFactory())
|
||||
|
||||
for index in range(3):
|
||||
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
|
||||
assert response.status_code == 201
|
||||
|
||||
response = client.post("/api/v1.0/rooms/", {"name": "Blocked room"})
|
||||
assert response.status_code == 429
|
||||
|
||||
now[0] += 24 * 60 * 60 + 1
|
||||
response = client.post("/api/v1.0/rooms/", {"name": "Next day room"})
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
def test_api_rooms_create_daily_throttle_per_user(daily_room_creation_throttle):
|
||||
"""Each user has its own daily cap."""
|
||||
client = APIClient()
|
||||
client.force_login(UserFactory())
|
||||
for index in range(3):
|
||||
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
|
||||
assert response.status_code == 201
|
||||
assert client.post("/api/v1.0/rooms/", {"name": "Blocked"}).status_code == 429
|
||||
|
||||
client.force_login(UserFactory())
|
||||
response = client.post("/api/v1.0/rooms/", {"name": "Other user room"})
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
def test_api_rooms_create_daily_throttle_does_not_limit_other_actions(
|
||||
daily_room_creation_throttle,
|
||||
):
|
||||
"""Reaching the daily cap leaves listing and updating available."""
|
||||
client = APIClient()
|
||||
client.force_login(UserFactory())
|
||||
for index in range(3):
|
||||
response = client.post("/api/v1.0/rooms/", {"name": f"Room {index}"})
|
||||
assert response.status_code == 201
|
||||
room_id = response.json()["id"]
|
||||
|
||||
assert client.post("/api/v1.0/rooms/", {"name": "Blocked"}).status_code == 429
|
||||
assert client.get("/api/v1.0/rooms/").status_code == 200
|
||||
response = client.patch(f"/api/v1.0/rooms/{room_id}/", {"name": "Renamed"})
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -361,6 +361,16 @@ class Base(Configuration):
|
||||
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
"DEFAULT_THROTTLE_RATES": {
|
||||
"room_creation": values.Value(
|
||||
default="50/minute",
|
||||
environ_name="ROOM_CREATION_THROTTLE_RATES",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"room_creation_daily": values.Value(
|
||||
default="1000/day",
|
||||
environ_name="ROOM_CREATION_DAILY_THROTTLE_RATES",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"request_entry": values.Value(
|
||||
default="150/minute",
|
||||
environ_name="REQUEST_ENTRY_THROTTLE_RATES",
|
||||
|
||||
@@ -45,6 +45,10 @@ export const ScreenRecordingSidePanel = () => {
|
||||
FeatureFlags.ScreenRecording
|
||||
)
|
||||
|
||||
const hasTranscriptAccess = useHasRecordingAccess(
|
||||
RecordingMode.Transcript,
|
||||
FeatureFlags.Transcript
|
||||
)
|
||||
const { notifyParticipants } = useNotifyParticipants()
|
||||
const { selectedLanguageKey, isLanguageSetToAuto } =
|
||||
useTranscriptionLanguage()
|
||||
@@ -88,7 +92,7 @@ export const ScreenRecordingSidePanel = () => {
|
||||
...(!isLanguageSetToAuto && {
|
||||
language: selectedLanguageKey,
|
||||
}),
|
||||
...(includeTranscript && { transcribe: true }),
|
||||
...(includeTranscript && hasTranscriptAccess && { transcribe: true }),
|
||||
}
|
||||
|
||||
await startRecording({
|
||||
@@ -182,24 +186,26 @@ export const ScreenRecordingSidePanel = () => {
|
||||
<RowWrapper iconName="mail" position="last">
|
||||
<Text variant="sm">{t('details.receiver')}</Text>
|
||||
</RowWrapper>
|
||||
|
||||
<div className={css({ height: '15px' })} />
|
||||
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
marginLeft: '20px',
|
||||
})}
|
||||
>
|
||||
<Checkbox
|
||||
size="sm"
|
||||
isSelected={includeTranscript}
|
||||
onChange={setIncludeTranscript}
|
||||
isDisabled={statuses.isActive || isPendingToStart}
|
||||
>
|
||||
<Text variant="sm">{t('details.transcription')}</Text>
|
||||
</Checkbox>
|
||||
</div>
|
||||
{hasTranscriptAccess && (
|
||||
<>
|
||||
<div className={css({ height: '15px' })} />
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
marginLeft: '20px',
|
||||
})}
|
||||
>
|
||||
<Checkbox
|
||||
size="sm"
|
||||
isSelected={includeTranscript}
|
||||
onChange={setIncludeTranscript}
|
||||
isDisabled={statuses.isActive || isPendingToStart}
|
||||
>
|
||||
<Text variant="sm">{t('details.transcription')}</Text>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
<ControlsButton
|
||||
i18nKeyPrefix={keyPrefix}
|
||||
|
||||
@@ -53,6 +53,10 @@ export const TranscriptSidePanel = () => {
|
||||
FeatureFlags.Transcript
|
||||
)
|
||||
|
||||
const hasScreenRecordingAccess = useHasRecordingAccess(
|
||||
RecordingMode.ScreenRecording,
|
||||
FeatureFlags.ScreenRecording
|
||||
)
|
||||
const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights(
|
||||
RecordingMode.Transcript,
|
||||
FeatureFlags.Transcript
|
||||
@@ -97,7 +101,9 @@ export const TranscriptSidePanel = () => {
|
||||
room.localParticipant
|
||||
)
|
||||
} else {
|
||||
const recordingMode = includeScreenRecording
|
||||
const withScreenRecording =
|
||||
includeScreenRecording && hasScreenRecordingAccess
|
||||
const recordingMode = withScreenRecording
|
||||
? RecordingMode.ScreenRecording
|
||||
: RecordingMode.Transcript
|
||||
|
||||
@@ -105,7 +111,7 @@ export const TranscriptSidePanel = () => {
|
||||
...(!isLanguageSetToAuto && {
|
||||
language: selectedLanguageKey,
|
||||
}),
|
||||
...(includeScreenRecording && {
|
||||
...(withScreenRecording && {
|
||||
transcribe: true,
|
||||
original_mode: RecordingMode.Transcript,
|
||||
}),
|
||||
@@ -122,7 +128,7 @@ export const TranscriptSidePanel = () => {
|
||||
type: NotificationType.TranscriptionStarted,
|
||||
})
|
||||
captureEvent('transcript-started', {
|
||||
includeScreenRecording: includeScreenRecording,
|
||||
includeScreenRecording: withScreenRecording,
|
||||
language: selectedLanguageKey,
|
||||
})
|
||||
}
|
||||
@@ -234,22 +240,26 @@ export const TranscriptSidePanel = () => {
|
||||
</Button>
|
||||
</Text>
|
||||
</RowWrapper>
|
||||
<div className={css({ height: '15px' })} />
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
marginLeft: '20px',
|
||||
})}
|
||||
>
|
||||
<Checkbox
|
||||
size="sm"
|
||||
isSelected={includeScreenRecording}
|
||||
onChange={setIncludeScreenRecording}
|
||||
isDisabled={statuses.isActive || isPendingToStart}
|
||||
>
|
||||
<Text variant="sm">{t('details.recording')}</Text>
|
||||
</Checkbox>
|
||||
</div>
|
||||
{hasScreenRecordingAccess && (
|
||||
<>
|
||||
<div className={css({ height: '15px' })} />
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
marginLeft: '20px',
|
||||
})}
|
||||
>
|
||||
<Checkbox
|
||||
size="sm"
|
||||
isSelected={includeScreenRecording}
|
||||
onChange={setIncludeScreenRecording}
|
||||
isDisabled={statuses.isActive || isPendingToStart}
|
||||
>
|
||||
<Text variant="sm">{t('details.recording')}</Text>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
<ControlsButton
|
||||
i18nKeyPrefix={keyPrefix}
|
||||
|
||||
Reference in New Issue
Block a user