mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-11 01:45:57 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a48f29abff |
@@ -8,17 +8,9 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- 📈(frontend) include LiveKit SIDs in the connection analytics event
|
||||
- 🔇(backend) silence expected 401 warnings on /me
|
||||
- 🔇(backend) silence noisy request summary info logs
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(backend) acknowledge unknown LiveKit webhook events instead of 422
|
||||
- 🔒️(backend) enforce display name setting on rename API
|
||||
- 🔒️(backend) reject inactive users in resource server backend
|
||||
|
||||
## [1.31.0] - 2026-09-08
|
||||
|
||||
|
||||
@@ -286,10 +286,6 @@ class ResourceServerBackend(LaSuiteBackend):
|
||||
if user is None and settings.OIDC_CREATE_USER:
|
||||
user = self.create_user(sub)
|
||||
|
||||
if user is not None and not user.is_active:
|
||||
logger.warning("Inactive user attempted authentication: %s", user.pk)
|
||||
raise SuspiciousOperation("User account is disabled.")
|
||||
|
||||
return user
|
||||
|
||||
def create_user(self, sub):
|
||||
|
||||
@@ -4,6 +4,7 @@ import copy
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
|
||||
@@ -73,7 +74,7 @@ class ApplicationViewSet(viewsets.ViewSet):
|
||||
except models.Application.DoesNotExist as e:
|
||||
raise drf_exceptions.AuthenticationFailed("Invalid credentials") from e
|
||||
|
||||
if not application.check_client_secret(client_secret):
|
||||
if not check_password(client_secret, application.client_secret):
|
||||
raise drf_exceptions.AuthenticationFailed("Invalid credentials")
|
||||
|
||||
if not application.is_active:
|
||||
|
||||
@@ -4,11 +4,9 @@ Core application fields
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from django.contrib.auth.hashers import identify_hasher
|
||||
from django.contrib.auth.hashers import identify_hasher, make_password
|
||||
from django.db import models
|
||||
|
||||
from .hashers import hash_client_secret
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@@ -26,14 +24,6 @@ class SecretField(models.CharField):
|
||||
|
||||
secret = getattr(model_instance, self.attname)
|
||||
|
||||
if secret.startswith("sha256$"):
|
||||
logger.debug(
|
||||
"%s: %s is already hashed with sha256.",
|
||||
model_instance,
|
||||
self.attname,
|
||||
)
|
||||
return secret
|
||||
|
||||
try:
|
||||
hasher = identify_hasher(secret)
|
||||
logger.debug(
|
||||
@@ -46,7 +36,7 @@ class SecretField(models.CharField):
|
||||
logger.debug(
|
||||
"%s: %s is not hashed; hashing it now.", model_instance, self.attname
|
||||
)
|
||||
hashed_secret = hash_client_secret(secret)
|
||||
hashed_secret = make_password(secret)
|
||||
setattr(model_instance, self.attname, hashed_secret)
|
||||
return hashed_secret
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"""Application secrets only: keep fast hashing out of PASSWORD_HASHERS.
|
||||
|
||||
Secrets must be securely randomly generated, not human-chosen.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.utils.crypto import constant_time_compare
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
|
||||
def hash_client_secret(raw_secret):
|
||||
"""Hash a machine-generated application secret without key stretching."""
|
||||
return f"sha256${hashlib.sha256(force_bytes(raw_secret)).hexdigest()}"
|
||||
|
||||
|
||||
def verify_client_secret(raw_secret, encoded):
|
||||
"""Verify the application format or a legacy Django password hash."""
|
||||
if raw_secret is None:
|
||||
return False
|
||||
if encoded.startswith("sha256$"):
|
||||
return constant_time_compare(encoded, hash_client_secret(raw_secret))
|
||||
return check_password(raw_secret, encoded)
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Logging filters for the core application."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class SilenceExpected401(logging.Filter):
|
||||
"""Drop the expected 401 from anonymous hits on the /me endpoint.
|
||||
|
||||
The frontend probes `/users/me/` to check authentication; a 401 for
|
||||
anonymous users is normal, not a warning worth logging.
|
||||
"""
|
||||
|
||||
def filter(self, record):
|
||||
"""Return False for a 401 on a silenced path, True otherwise."""
|
||||
if getattr(record, "status_code", None) != 401:
|
||||
return True
|
||||
|
||||
request = getattr(record, "request", None)
|
||||
path = getattr(request, "path", None)
|
||||
if not path:
|
||||
return True
|
||||
|
||||
return path not in settings.LOGGING_SILENCED_401_PATHS
|
||||
@@ -25,7 +25,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from lasuite.tools.email import get_domain_from_email
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
from . import fields, hashers, utils
|
||||
from . import fields, utils
|
||||
from .recording.enums import FileExtension
|
||||
from .validators import sub_validator
|
||||
|
||||
@@ -828,31 +828,6 @@ class Application(BaseModel):
|
||||
def __str__(self):
|
||||
return f"{self.name!s}"
|
||||
|
||||
def check_client_secret(self, raw_secret):
|
||||
"""Verify and lazily rehash without overwriting a concurrent rotation."""
|
||||
original_hash = self.client_secret
|
||||
if not hashers.verify_client_secret(raw_secret, original_hash):
|
||||
return False
|
||||
|
||||
if original_hash.startswith("sha256$"):
|
||||
return True
|
||||
|
||||
# Fast hashing assumes securely generated, high-entropy secrets.
|
||||
# APPLICATION_CLIENT_SECRET_LENGTH controls generated length, not randomness.
|
||||
encoded = hashers.hash_client_secret(raw_secret)
|
||||
updated = Application.objects.filter(
|
||||
pk=self.pk, client_secret=original_hash
|
||||
).update(client_secret=encoded)
|
||||
if updated:
|
||||
self.client_secret = encoded
|
||||
return True
|
||||
|
||||
try:
|
||||
self.refresh_from_db()
|
||||
except type(self).DoesNotExist:
|
||||
return False
|
||||
return hashers.verify_client_secret(raw_secret, self.client_secret)
|
||||
|
||||
def can_delegate_email(self, email):
|
||||
"""Check if this application can delegate the given email."""
|
||||
|
||||
|
||||
@@ -52,6 +52,12 @@ class InvalidPayloadError(LiveKitWebhookError):
|
||||
status_code = 400
|
||||
|
||||
|
||||
class UnsupportedEventTypeError(LiveKitWebhookError):
|
||||
"""Unsupported event type."""
|
||||
|
||||
status_code = 422
|
||||
|
||||
|
||||
class ActionFailedError(LiveKitWebhookError):
|
||||
"""Webhook action fails to process or complete."""
|
||||
|
||||
@@ -68,7 +74,6 @@ class LiveKitWebhookEventType(Enum):
|
||||
# Participant events
|
||||
PARTICIPANT_JOINED = "participant_joined"
|
||||
PARTICIPANT_LEFT = "participant_left"
|
||||
PARTICIPANT_CONNECTION_ABORTED = "participant_connection_aborted"
|
||||
|
||||
# Track events
|
||||
TRACK_PUBLISHED = "track_published"
|
||||
@@ -148,13 +153,10 @@ class LiveKitEventsService:
|
||||
|
||||
try:
|
||||
webhook_type = LiveKitWebhookEventType(data.event)
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
"Ignoring unknown LiveKit webhook event type '%s' for room '%s'",
|
||||
data.event,
|
||||
room_name,
|
||||
)
|
||||
return
|
||||
except ValueError as e:
|
||||
raise UnsupportedEventTypeError(
|
||||
f"Unknown webhook type: {data.event}"
|
||||
) from e
|
||||
|
||||
# Handle according to received webhook type
|
||||
handler = self._webhook_handlers.get(webhook_type.value)
|
||||
|
||||
@@ -94,7 +94,7 @@ def test_invalid_payload(client, auth_token, mock_livekit_config):
|
||||
|
||||
|
||||
def test_unknown_event_type(client, mock_livekit_config):
|
||||
"""Should acknowledge (200) an unknown event type rather than reject it."""
|
||||
"""Should return 422 for unknown event type."""
|
||||
event_data = json.dumps({"event": "unknown_event_type"})
|
||||
|
||||
# Generate auth token for this specific payload
|
||||
@@ -112,8 +112,10 @@ def test_unknown_event_type(client, mock_livekit_config):
|
||||
HTTP_AUTHORIZATION=auth_token,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "success"}
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"status": "error",
|
||||
}
|
||||
|
||||
|
||||
@mock.patch.object(LiveKitEventsService, "_handle_room_finished")
|
||||
|
||||
@@ -16,6 +16,7 @@ from core.services.livekit_events import (
|
||||
AuthenticationError,
|
||||
InvalidPayloadError,
|
||||
LiveKitEventsService,
|
||||
UnsupportedEventTypeError,
|
||||
api,
|
||||
)
|
||||
from core.services.lobby import LobbyService
|
||||
@@ -664,27 +665,22 @@ def test_receive_missing_auth(service):
|
||||
|
||||
|
||||
@mock.patch.object(api.WebhookReceiver, "receive")
|
||||
def test_receive_unknown_event_is_acknowledged(mock_receive, service, caplog):
|
||||
"""Unknown event types are logged and ignored, not rejected.
|
||||
|
||||
LiveKit adds event types over time and does not retry 4xx responses, so
|
||||
raising here would silently drop the event.
|
||||
"""
|
||||
def test_receive_unsupported_event(mock_receive, service):
|
||||
"""Should raise LiveKitWebhookError for unsupported events."""
|
||||
mock_request = mock.MagicMock()
|
||||
mock_request.headers = {"Authorization": "test_token"}
|
||||
mock_request.body = b"{}"
|
||||
|
||||
# Mock returned data with unsupported event type
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.room.name = str(uuid.uuid4())
|
||||
mock_data.event = "some_future_event"
|
||||
mock_data.event = "unsupported_event"
|
||||
mock_receive.return_value = mock_data
|
||||
|
||||
with caplog.at_level("WARNING", logger="core.services.livekit_events"):
|
||||
service.receive(mock_request) # must not raise
|
||||
|
||||
assert "Ignoring unknown LiveKit webhook event type 'some_future_event'" in (
|
||||
caplog.text
|
||||
)
|
||||
with pytest.raises(
|
||||
UnsupportedEventTypeError, match="Unknown webhook type: unsupported_event"
|
||||
):
|
||||
service.receive(mock_request)
|
||||
|
||||
|
||||
@mock.patch.object(api.WebhookReceiver, "receive")
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
"""Application hashing and migration of existing credentials."""
|
||||
|
||||
import hashlib
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.hashers import check_password, identify_hasher, make_password
|
||||
from django.db import connection
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import hashers
|
||||
from core.factories import ApplicationFactory, UserFactory
|
||||
from core.models import Application
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.mark.parametrize("secret", ["short", "a" * 128, b"byte-secret"])
|
||||
def test_application_hash(secret):
|
||||
"""Application hashes verify correctly but are not accepted for user passwords."""
|
||||
encoded = hashers.hash_client_secret(secret)
|
||||
raw = secret.encode() if isinstance(secret, str) else secret
|
||||
assert encoded == f"sha256${hashlib.sha256(raw).hexdigest()}"
|
||||
assert hashers.verify_client_secret(secret, encoded)
|
||||
assert not hashers.verify_client_secret("wrong", encoded)
|
||||
assert not hashers.verify_client_secret(None, encoded)
|
||||
assert not hashers.verify_client_secret(secret, "sha256$invalid")
|
||||
assert not check_password(secret, encoded)
|
||||
with pytest.raises(ValueError):
|
||||
identify_hasher(encoded)
|
||||
assert not make_password(raw.decode()).startswith("sha256$")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("algorithm", ["pbkdf2_sha256", "md5"])
|
||||
def test_token_migrates_legacy_secret_once(algorithm):
|
||||
"""The same client secret works before and after migration, with no later writes."""
|
||||
secret = get_random_string(128)
|
||||
user = UserFactory()
|
||||
legacy = make_password(secret, hasher=algorithm)
|
||||
app = ApplicationFactory(client_secret=legacy)
|
||||
app.refresh_from_db()
|
||||
|
||||
assert app.client_secret == legacy
|
||||
payload = {
|
||||
"client_id": app.client_id,
|
||||
"client_secret": secret,
|
||||
"grant_type": "client_credentials",
|
||||
"scope": user.email,
|
||||
}
|
||||
client = APIClient()
|
||||
response = client.post(
|
||||
"/external-api/v1.0/application/token/", payload, format="json"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
app.refresh_from_db()
|
||||
migrated = app.client_secret
|
||||
assert migrated == hashers.hash_client_secret(secret)
|
||||
with CaptureQueriesContext(connection) as queries:
|
||||
response = client.post(
|
||||
"/external-api/v1.0/application/token/", payload, format="json"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert not any(q["sql"].lstrip().startswith("UPDATE") for q in queries)
|
||||
app.refresh_from_db()
|
||||
assert app.client_secret == migrated
|
||||
|
||||
|
||||
def test_wrong_secret_does_not_migrate():
|
||||
"""Failed authentication leaves a production PBKDF2 hash untouched."""
|
||||
user = UserFactory()
|
||||
legacy = make_password(get_random_string(128), hasher="pbkdf2_sha256")
|
||||
app = ApplicationFactory(client_secret=legacy)
|
||||
response = APIClient().post(
|
||||
"/external-api/v1.0/application/token/",
|
||||
{
|
||||
"client_id": app.client_id,
|
||||
"client_secret": "wrong",
|
||||
"grant_type": "client_credentials",
|
||||
"scope": user.email,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 401
|
||||
app.refresh_from_db()
|
||||
assert app.client_secret == legacy
|
||||
|
||||
|
||||
def test_migration_preserves_concurrent_rotation():
|
||||
"""Migration must not restore a secret rotated after verification."""
|
||||
secret = get_random_string(128)
|
||||
app = ApplicationFactory(
|
||||
client_secret=make_password(secret, hasher="pbkdf2_sha256")
|
||||
)
|
||||
replacement = hashers.hash_client_secret(get_random_string(128))
|
||||
|
||||
def verify_then_rotate(raw, encoded):
|
||||
verified = check_password(raw, encoded)
|
||||
Application.objects.filter(pk=app.pk).update(client_secret=replacement)
|
||||
return verified
|
||||
|
||||
with mock.patch.object(hashers, "check_password", side_effect=verify_then_rotate):
|
||||
assert app.check_client_secret(secret) is False
|
||||
|
||||
app.refresh_from_db()
|
||||
assert app.client_secret == replacement
|
||||
|
||||
|
||||
def test_migration_preserves_concurrent_migration():
|
||||
"""Authentication succeeds when another request migrates the same secret."""
|
||||
secret = get_random_string(128)
|
||||
app = ApplicationFactory(
|
||||
client_secret=make_password(secret, hasher="pbkdf2_sha256")
|
||||
)
|
||||
migrated = hashers.hash_client_secret(secret)
|
||||
|
||||
def verify_then_migrate(raw, encoded):
|
||||
verified = check_password(raw, encoded)
|
||||
Application.objects.filter(pk=app.pk).update(client_secret=migrated)
|
||||
return verified
|
||||
|
||||
with mock.patch.object(hashers, "check_password", side_effect=verify_then_migrate):
|
||||
assert app.check_client_secret(secret) is True
|
||||
|
||||
app.refresh_from_db()
|
||||
assert app.client_secret == migrated
|
||||
|
||||
|
||||
def test_migration_preserves_concurrent_deletion():
|
||||
"""Authentication fails when the application is deleted after verification."""
|
||||
secret = get_random_string(128)
|
||||
app = ApplicationFactory(
|
||||
client_secret=make_password(secret, hasher="pbkdf2_sha256")
|
||||
)
|
||||
|
||||
def verify_then_delete(raw, encoded):
|
||||
verified = check_password(raw, encoded)
|
||||
Application.objects.filter(pk=app.pk).delete()
|
||||
return verified
|
||||
|
||||
with mock.patch.object(hashers, "check_password", side_effect=verify_then_delete):
|
||||
assert app.check_client_secret(secret) is False
|
||||
|
||||
assert not Application.objects.filter(pk=app.pk).exists()
|
||||
@@ -1,96 +0,0 @@
|
||||
"""Tests for the external API ResourceServerBackend."""
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core.external_api.authentication import ResourceServerBackend
|
||||
from core.factories import UserFactory
|
||||
from core.models import User
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def _payload(sub):
|
||||
return {"sub": sub, "active": True, "scope": "lasuite_meet", "client_id": "app"}
|
||||
|
||||
|
||||
def test_resource_server_backend_get_or_create_user_active():
|
||||
"""An existing active user matching the sub should be returned."""
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
result = ResourceServerBackend().get_or_create_user(
|
||||
access_token="token", id_token=None, payload=_payload(user.sub)
|
||||
)
|
||||
|
||||
assert result == user
|
||||
|
||||
|
||||
def test_resource_server_backend_get_or_create_user_inactive():
|
||||
"""An inactive user should be rejected even with a valid token."""
|
||||
|
||||
user = UserFactory(is_active=False)
|
||||
|
||||
with pytest.raises(SuspiciousOperation, match="User account is disabled."):
|
||||
ResourceServerBackend().get_or_create_user(
|
||||
access_token="token", id_token=None, payload=_payload(user.sub)
|
||||
)
|
||||
|
||||
|
||||
def test_resource_server_backend_get_or_create_user_creates(settings):
|
||||
"""An unknown sub should create an active user when OIDC_CREATE_USER is set."""
|
||||
|
||||
settings.OIDC_CREATE_USER = True
|
||||
|
||||
result = ResourceServerBackend().get_or_create_user(
|
||||
access_token="token", id_token=None, payload=_payload("new-sub")
|
||||
)
|
||||
|
||||
assert result.sub == "new-sub"
|
||||
assert result.is_active is True
|
||||
assert User.objects.filter(sub="new-sub").exists()
|
||||
|
||||
|
||||
def test_resource_server_backend_get_or_create_user_no_creation(settings):
|
||||
"""An unknown sub should return None when OIDC_CREATE_USER is unset."""
|
||||
|
||||
settings.OIDC_CREATE_USER = False
|
||||
|
||||
result = ResourceServerBackend().get_or_create_user(
|
||||
access_token="token", id_token=None, payload=_payload("new-sub")
|
||||
)
|
||||
|
||||
assert result is None
|
||||
assert not User.objects.filter(sub="new-sub").exists()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_rooms_list_resource_server_inactive_user(settings):
|
||||
"""End to end: a valid introspected token for an inactive user should get 401."""
|
||||
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
|
||||
user = UserFactory(is_active=False)
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={
|
||||
"iss": "https://oidc.example.com",
|
||||
"active": True,
|
||||
"sub": user.sub,
|
||||
"scope": "openid lasuite_meet rooms:list",
|
||||
"client_id": "app",
|
||||
},
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer rs-token")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "login failed" in str(response.data).lower()
|
||||
@@ -6,12 +6,12 @@ Unit tests for the Application and ApplicationDomain models
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
import pytest
|
||||
|
||||
from core.factories import ApplicationDomainFactory, ApplicationFactory
|
||||
from core.hashers import verify_client_secret
|
||||
from core.models import Application, ApplicationDomain, ApplicationScope
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
@@ -98,8 +98,8 @@ def test_models_application_client_secret_hashed_on_save():
|
||||
|
||||
# Secret should be hashed, not plain
|
||||
assert application.client_secret != plain_secret
|
||||
# Should verify with the application credential policy
|
||||
assert verify_client_secret(plain_secret, application.client_secret) is True
|
||||
# Should verify with check_password
|
||||
assert check_password(plain_secret, application.client_secret) is True
|
||||
|
||||
|
||||
def test_models_application_client_secret_preserves_existing_hash():
|
||||
|
||||
@@ -469,9 +469,6 @@ class Base(Configuration):
|
||||
|
||||
# Sentry
|
||||
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN")
|
||||
SENTRY_TRACES_SAMPLE_RATE = values.FloatValue(
|
||||
0.0, environ_name="SENTRY_TRACES_SAMPLE_RATE", environ_prefix=None
|
||||
)
|
||||
|
||||
# Easy thumbnails
|
||||
THUMBNAIL_EXTENSION = "webp"
|
||||
@@ -1113,12 +1110,6 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
LOGGING_SILENCED_401_PATHS = values.ListValue(
|
||||
default=["/api/v1.0/users/me/"],
|
||||
environ_name="LOGGING_SILENCED_401_PATHS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Logging
|
||||
# We want to make it easy to log to console but by default we log production
|
||||
# to Sentry and don't want to log to console.
|
||||
@@ -1131,16 +1122,10 @@ class Base(Configuration):
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"filters": {
|
||||
"silence_expected_401": {
|
||||
"()": "core.logging_filters.SilenceExpected401",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "simple",
|
||||
"filters": ["silence_expected_401"],
|
||||
},
|
||||
},
|
||||
# Override root logger to send it to console
|
||||
@@ -1151,13 +1136,6 @@ class Base(Configuration):
|
||||
),
|
||||
},
|
||||
"loggers": {
|
||||
"request.summary": {
|
||||
"level": values.Value(
|
||||
"WARNING",
|
||||
environ_name="LOGGING_LEVEL_REQUEST_SUMMARY",
|
||||
environ_prefix="",
|
||||
)
|
||||
},
|
||||
"core": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
@@ -1233,14 +1211,7 @@ class Base(Configuration):
|
||||
dsn=cls.SENTRY_DSN,
|
||||
environment=cls.__name__.lower(), # build, test, development, production
|
||||
release=get_release(),
|
||||
traces_sample_rate=cls.SENTRY_TRACES_SAMPLE_RATE,
|
||||
integrations=[
|
||||
DjangoIntegration(
|
||||
transaction_style="url",
|
||||
middleware_spans=True,
|
||||
cache_spans=True,
|
||||
)
|
||||
],
|
||||
integrations=[DjangoIntegration()],
|
||||
)
|
||||
sentry_sdk.set_tag("application", "backend")
|
||||
|
||||
@@ -1312,7 +1283,6 @@ class Test(Base):
|
||||
)
|
||||
PASSWORD_HASHERS = [
|
||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
||||
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
||||
]
|
||||
USE_SWAGGER = True
|
||||
EXTERNAL_API_ENABLED = True
|
||||
|
||||
@@ -71,30 +71,20 @@ export const ConnectionObserver = () => {
|
||||
useEffect(() => {
|
||||
if (!isAnalyticsEnabled) return
|
||||
|
||||
const handleConnection = async () => {
|
||||
const handleConnection = () => {
|
||||
// Preserve original connection timestamp across reconnections to measure
|
||||
// total session duration from first connect to final disconnect.
|
||||
if (connectionStartTimeRef.current != null) return
|
||||
connectionStartTimeRef.current = Date.now()
|
||||
const participantSid = room.localParticipant.sid
|
||||
const roomSid = await room.getSid().catch(() => undefined)
|
||||
void captureMediaEvent('connection-event', {
|
||||
livekit_room_sid: roomSid,
|
||||
livekit_participant_sid: participantSid,
|
||||
})
|
||||
void captureMediaEvent('connection-event', {})
|
||||
}
|
||||
|
||||
const handleReconnect = () => {
|
||||
captureEvent('reconnect-event')
|
||||
}
|
||||
|
||||
const handleReconnected = async () => {
|
||||
const participantSid = room.localParticipant.sid
|
||||
const roomSid = await room.getSid().catch(() => undefined)
|
||||
captureEvent('reconnected-event', {
|
||||
livekit_room_sid: roomSid,
|
||||
livekit_participant_sid: participantSid,
|
||||
})
|
||||
const handleReconnected = () => {
|
||||
captureEvent('reconnected-event')
|
||||
}
|
||||
|
||||
const handleSignalingConnect = () => {
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "Ein neues Dokument wird erstellt auf",
|
||||
"destinationUnknown": "Ein neues Dokument wird erstellt",
|
||||
"language": "Meeting-Sprache:",
|
||||
"recording": "Auch eine Videoaufzeichnung starten"
|
||||
"recording": "Auch eine Aufzeichnung starten"
|
||||
},
|
||||
"button": {
|
||||
"start": "Meeting-Transkription starten",
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "A new document will be created on",
|
||||
"destinationUnknown": "A new document will be created",
|
||||
"language": "Meeting language:",
|
||||
"recording": "Also start a video recording"
|
||||
"recording": "Also start a recording"
|
||||
},
|
||||
"button": {
|
||||
"start": "Start transcribing the meeting",
|
||||
|
||||
@@ -479,7 +479,7 @@
|
||||
"destination": "Se creará un nuevo documento en",
|
||||
"destinationUnknown": "Se creará un nuevo documento",
|
||||
"language": "Idioma de la reunión:",
|
||||
"recording": "Iniciar también una grabación de vídeo"
|
||||
"recording": "Iniciar también una grabación"
|
||||
},
|
||||
"button": {
|
||||
"start": "Empezar a transcribir la reunión",
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "Un nouveau document sera créé sur",
|
||||
"destinationUnknown": "Un nouveau document sera créé",
|
||||
"language": "Langue de la réunion :",
|
||||
"recording": "Démarrer aussi un enregistrement vidéo"
|
||||
"recording": "Démarrer aussi un enregistrement"
|
||||
},
|
||||
"button": {
|
||||
"start": "Commencer à transcrire la réunion",
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "Er wordt een nieuw document aangemaakt op",
|
||||
"destinationUnknown": "Een nieuw document wordt aangemaakt",
|
||||
"language": "Vergadertalen:",
|
||||
"recording": "Start ook een video-opname"
|
||||
"recording": "Start ook een opname"
|
||||
},
|
||||
"button": {
|
||||
"start": "Begin met het transcriberen van de vergadering",
|
||||
|
||||
Reference in New Issue
Block a user