Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2861aa143e | |||
| 4748b55089 | |||
| ce9f812a7e | |||
| b02591170f | |||
| e58181f846 | |||
| d37f47e82c | |||
| db80c09c10 | |||
| fd9f2a81ca | |||
| d865db5f1b | |||
| 7cc5b2b961 | |||
| c85977cb68 | |||
| 3c3b4a32e3 | |||
| 9b033c55b2 | |||
| a2c7becaf4 | |||
| 89031abb63 | |||
| fc92fa4eb4 | |||
| 2c65cc061e | |||
| bfadeae6ee | |||
| 117677bd14 | |||
| 69c6e58017 | |||
| 6742f5d19d | |||
| 23de7e52bc | |||
| 3887255e9c | |||
| 5d6ad3f3f6 | |||
| 44d68a9c80 | |||
| ed5c1bbd84 | |||
| f8c6da8021 | |||
| 5ba1657e00 | |||
| c28b8ba902 | |||
| 6962367e18 | |||
| 0bd57e8623 | |||
| 27f2023104 | |||
| 44362eca23 | |||
| c34a85699b | |||
| 12d8c4a9db | |||
| 42a05da5c0 | |||
| 4344dd6e35 | |||
| fe28902b2e | |||
| 1e1e1a2657 |
@@ -10,9 +10,39 @@ and this project adheres to
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(frontend) Use React Aria FocusScope for side panel accessibility
|
||||
|
||||
## [1.7.0] - 2026-02-19
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) expose Windows app web link #976
|
||||
- ✨(frontend) support additional shortcuts to broaden accessibility
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(frontend) add clickable settings general link in idle modal #974
|
||||
|
||||
## [1.6.0] - 2026-02-10
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) monitor throttling rate failure through sentry #964
|
||||
- 🚀(paas) add PaaS deployment scripts, tested on Scalingo #957
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿️(frontend) improve spinner reduced‑motion fallback #931
|
||||
- ♿️(frontend) fix form labels and autocomplete wiring #932
|
||||
- 🥅(summary) catch file-related exceptions when handling recording #944
|
||||
- 📝(frontend) update legal terms #956
|
||||
- ⚡️(backend) enhance django admin's loading performance #954
|
||||
- 🌐(frontend) add missing DE translation for accessibility settings
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔐(backend) enforce object-level permission checks on room endpoint #959
|
||||
- 🔒️(backend) add application validation when consuming external JWT #963
|
||||
|
||||
## [1.5.0] - 2026-01-28
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
web: bin/buildpack_start.sh
|
||||
postdeploy: python manage.py migrate
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit # always exit on error
|
||||
set -o pipefail # don't ignore exit codes when piping output
|
||||
|
||||
echo "-----> Running post-compile script"
|
||||
|
||||
# Cleanup
|
||||
rm -rf docker docs env.d gitlint
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit # always exit on error
|
||||
set -o pipefail # don't ignore exit codes when piping output
|
||||
|
||||
echo "-----> Running post-frontend script"
|
||||
|
||||
# Move the frontend build to the nginx root and clean up
|
||||
mkdir -p build/
|
||||
mv src/frontend/dist build/frontend-out
|
||||
|
||||
ASSETS_DIR=build/frontend-out/assets
|
||||
if [ -n "$CUSTOM_LOGO_URL" ]; then
|
||||
# Ensure https
|
||||
[[ ! "$CUSTOM_LOGO_URL" =~ ^https:// ]] && echo "[custom-logo] ERROR: URL must use HTTPS" >&2 && exit 1
|
||||
|
||||
# Prevent SSRF
|
||||
HOSTNAME=$(echo "$CUSTOM_LOGO_URL" | sed -E 's|^https://([^/:]+).*|\1|')
|
||||
[[ "$HOSTNAME" =~ ^(localhost|127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|0\.0\.0\.0|\[::1\]) ]] && echo "[custom-logo] ERROR: SSRF blocked: $HOSTNAME" >&2 && exit 1
|
||||
|
||||
LOGO_FILE="${ASSETS_DIR}/logo.svg"
|
||||
TMP_FILE=$(mktemp "${LOGO_FILE}.XXXXXX.tmp")
|
||||
|
||||
# Actual download
|
||||
echo "[custom-logo] INFO: Downloading custom logo from: $CUSTOM_LOGO_URL"
|
||||
curl -fsSL --tlsv1.2 -o "$TMP_FILE" "$CUSTOM_LOGO_URL"
|
||||
|
||||
# Validate filesize
|
||||
FILESIZE=$(stat -c%s "$TMP_FILE" 2>/dev/null || stat -f%z "$TMP_FILE")
|
||||
[[ "$FILESIZE" -eq 0 ]] && echo "[custom-logo] ERROR: empty file" >&2 && exit 1
|
||||
[[ "$FILESIZE" -gt 5242880 ]] && echo "[custom-logo] ERROR: file too large (${FILESIZE}B > 5MB)" >&2 && exit 1
|
||||
|
||||
# Validate file type
|
||||
IS_SVG=false
|
||||
|
||||
HEADER=$(head -c 100 "$TMP_FILE" | tr -d '\0' | tr '[:upper:]' '[:lower:]')
|
||||
[[ "$HEADER" =~ ^.*"<svg".*$ ]] && IS_SVG=true
|
||||
[[ "$HEADER" =~ ^.*"<?xml".*"<svg".*$ ]] && IS_SVG=true
|
||||
|
||||
[[ "$IS_SVG" == false ]] && echo "[custom-logo] ERROR: not a valid SVG file" >&2 && exit 1
|
||||
|
||||
mv -f "$TMP_FILE" "$LOGO_FILE"
|
||||
echo "[custom-logo] INFO: Custom logo downloaded successfuly"
|
||||
fi
|
||||
|
||||
mv src/backend/* ./
|
||||
mv deploy/paas/* ./
|
||||
|
||||
echo "3.13" > .python-version
|
||||
echo "." > requirements.txt
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start the Django backend server
|
||||
gunicorn -b 0.0.0.0:8000 meet.wsgi:application --log-file - &
|
||||
|
||||
# Start the Nginx server
|
||||
bin/run &
|
||||
|
||||
# if the current shell is killed, also terminate all its children
|
||||
trap "pkill SIGTERM -P $$" SIGTERM
|
||||
|
||||
# wait for a single child to finish,
|
||||
wait -n
|
||||
# then kill all the other tasks
|
||||
pkill -P $$
|
||||
@@ -0,0 +1,52 @@
|
||||
# ERB templated nginx configuration
|
||||
# see https://doc.scalingo.com/platform/deployment/buildpacks/nginx
|
||||
|
||||
upstream backend_server {
|
||||
server localhost:8000 fail_timeout=0;
|
||||
}
|
||||
|
||||
server {
|
||||
listen <%= ENV["PORT"] %>;
|
||||
server_name _;
|
||||
server_tokens off;
|
||||
|
||||
root /app/build/frontend-out;
|
||||
|
||||
# Django rest framework
|
||||
location ^~ /api/ {
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_pass http://backend_server;
|
||||
}
|
||||
|
||||
# Django admin
|
||||
location ^~ /admin/ {
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_pass http://backend_server;
|
||||
}
|
||||
|
||||
# Serve static files with caching
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
# Serve static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
# Add no-cache headers
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache"; # HTTP 1.0 header for backward compatibility
|
||||
add_header Expires 0;
|
||||
}
|
||||
|
||||
# Optionally, handle 404 errors by redirecting to index.html
|
||||
error_page 404 =200 /index.html;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
upstream meet_backend {
|
||||
server ${BACKEND_HOST}:8000 fail_timeout=0;
|
||||
server ${BACKEND_INTERNAL_HOST}:8000 fail_timeout=0;
|
||||
}
|
||||
|
||||
upstream meet_frontend {
|
||||
server ${FRONTEND_HOST}:8080 fail_timeout=0;
|
||||
server ${FRONTEND_INTERNAL_HOST}:8080 fail_timeout=0;
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
@@ -20,8 +20,8 @@ services:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
- env.d/common
|
||||
- env.d/backend
|
||||
- env.d/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "manage.py", "check"]
|
||||
@@ -45,6 +45,7 @@ services:
|
||||
- /docker-entrypoint.sh
|
||||
command: ["nginx", "-g", "daemon off;"]
|
||||
env_file:
|
||||
- .env
|
||||
- env.d/common
|
||||
# Uncomment and set your values if using our nginx proxy example
|
||||
# environment:
|
||||
|
||||
@@ -9,6 +9,10 @@ La Suite Meet maintainers use only the Kubernetes deployment method in productio
|
||||
We understand that not everyone has a Kubernetes cluster available, please follow the instructions provided [here](/docs/installation/compose.md) to set up a docker compose instance.
|
||||
We also provide [Docker images](https://hub.docker.com/u/lasuite?page=1&search=meet) that can be deployed using Compose.
|
||||
|
||||
## Scalingo
|
||||
|
||||
La Suite Meet can be deployed on Scalingo PaaS using the Suite Numérique buildpack. See the [Scalingo deployment guide](./scalingo.md) for detailed instructions.
|
||||
|
||||
## Other ways to install La Suite Meet
|
||||
Community members have contributed alternative ways to install La Suite Meet 🙏. While maintainers may not provide direct support, we help keep these instructions up to date, and you can reach out to contributors or the community for assistance.
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# Deployment on Scalingo
|
||||
|
||||
This guide explains how to deploy La Suite Meet on [Scalingo](https://scalingo.com/) using the [Suite Numérique buildpack](https://github.com/suitenumerique/buildpack).
|
||||
|
||||
## Overview
|
||||
|
||||
Scalingo is a Platform-as-a-Service (PaaS) that simplifies application deployment. This setup uses a custom buildpack to handle both the frontend (Vite) and backend (Django) builds, serving them through Nginx.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Scalingo account
|
||||
- Scalingo CLI installed (optional but recommended)
|
||||
- A PostgreSQL database addon
|
||||
- A Redis addon (for caching and sessions)
|
||||
|
||||
## Step 1: Create Your App
|
||||
|
||||
Create a new app on Scalingo using `scalingo` cli or using the [Scalingo dashboard](https://dashboard.scalingo.com/).
|
||||
|
||||
## Step 2: Provision Addons
|
||||
|
||||
Add the required PostgreSQL and Redis services.
|
||||
|
||||
This will set the following environment variables automatically:
|
||||
- `SCALINGO_POSTGRESQL_URL` - Database connection string
|
||||
- `SCALINGO_REDIS_URL` - Redis connection string
|
||||
|
||||
## Step 3: Configure Environment Variables
|
||||
|
||||
Set the following environment variables in your Scalingo app:
|
||||
|
||||
### Buildpack Configuration
|
||||
|
||||
```bash
|
||||
scalingo env-set BUILDBACK_URL="https://github.com/suitenumerique/buildpack#main"
|
||||
scalingo env-set LASUITE_APP_NAME="meet"
|
||||
scalingo env-set LASUITE_BACKEND_DIR="."
|
||||
scalingo env-set LASUITE_FRONTEND_DIR="src/frontend/"
|
||||
scalingo env-set LASUITE_NGINX_DIR="."
|
||||
scalingo env-set LASUITE_SCRIPT_POSTCOMPILE="bin/buildpack_postcompile.sh"
|
||||
scalingo env-set LASUITE_SCRIPT_POSTFRONTEND="bin/buildpack_postfrontend.sh"
|
||||
```
|
||||
|
||||
### Database and Cache
|
||||
|
||||
```bash
|
||||
scalingo env-set DATABASE_URL="\$SCALINGO_POSTGRESQL_URL"
|
||||
scalingo env-set REDIS_URL="\$SCALINGO_REDIS_URL"
|
||||
```
|
||||
|
||||
### Django Settings
|
||||
|
||||
```bash
|
||||
scalingo env-set DJANGO_SETTINGS_MODULE="meet.settings"
|
||||
scalingo env-set DJANGO_CONFIGURATION="Production"
|
||||
scalingo env-set DJANGO_SECRET_KEY="<generate-a-secure-secret-key>"
|
||||
scalingo env-set DJANGO_ALLOWED_HOSTS="my-meet-app.osc-fr1.scalingo.io"
|
||||
```
|
||||
|
||||
### OIDC Authentication
|
||||
|
||||
Configure your OIDC provider (e.g., Keycloak, Authentik):
|
||||
|
||||
```bash
|
||||
scalingo env-set OIDC_OP_BASE_URL="https://auth.yourdomain.com/realms/meet"
|
||||
scalingo env-set OIDC_RP_CLIENT_ID="meet-client-id"
|
||||
scalingo env-set OIDC_RP_CLIENT_SECRET="<your-client-secret>"
|
||||
scalingo env-set OIDC_RP_SIGN_ALGO="RS256"
|
||||
```
|
||||
|
||||
### LiveKit Configuration
|
||||
|
||||
Meet requires a LiveKit server for video conferencing:
|
||||
|
||||
```bash
|
||||
scalingo env-set LIVEKIT_API_URL="wss://livekit.yourdomain.com"
|
||||
scalingo env-set LIVEKIT_API_KEY="<your-livekit-api-key>"
|
||||
scalingo env-set LIVEKIT_API_SECRET="<your-livekit-api-secret>"
|
||||
```
|
||||
|
||||
### Email Configuration (Optional)
|
||||
|
||||
For email notifications see https://doc.scalingo.com/platform/app/sending-emails:
|
||||
|
||||
```bash
|
||||
scalingo env-set DJANGO_EMAIL_HOST="smtp.example.org"
|
||||
scalingo env-set DJANGO_EMAIL_PORT="587"
|
||||
scalingo env-set DJANGO_EMAIL_HOST_USER="<smtp-user>"
|
||||
scalingo env-set DJANGO_EMAIL_HOST_PASSWORD="<smtp-password>"
|
||||
scalingo env-set DJANGO_EMAIL_USE_TLS="True"
|
||||
scalingo env-set DJANGO_EMAIL_FROM="meet@yourdomain.com"
|
||||
```
|
||||
|
||||
## Step 4: Deploy
|
||||
|
||||
Deploy your application:
|
||||
|
||||
```bash
|
||||
git push scalingo main
|
||||
```
|
||||
|
||||
The Procfile will automatically:
|
||||
1. Build the frontend (Vite)
|
||||
2. Build the backend (Django)
|
||||
3. Run the post-compile script (cleanup)
|
||||
4. Run the post-frontend script (move assets and prepare for deployment)
|
||||
5. Start Nginx and Gunicorn
|
||||
6. Run django migrations
|
||||
|
||||
## Step 5: Create superuser
|
||||
|
||||
After the first deployment, create an admin user:
|
||||
|
||||
```bash
|
||||
scalingo run python manage.py createsuperuser
|
||||
```
|
||||
|
||||
## Custom Domain (Optional)
|
||||
|
||||
To use a custom domain:
|
||||
|
||||
1. Add the domain in Scalingo dashboard
|
||||
2. Update `DJANGO_ALLOWED_HOSTS` with your custom domain
|
||||
3. Configure your DNS to point to Scalingo
|
||||
|
||||
```bash
|
||||
scalingo domains-add meet.yourdomain.com
|
||||
scalingo env-set DJANGO_ALLOWED_HOSTS="meet.yourdomain.com,my-meet-app.osc-fr1.scalingo.io"
|
||||
```
|
||||
|
||||
## Custom Logo (Optional)
|
||||
|
||||
To use a custom logo, set the `CUSTOM_LOGO_URL` environment variable with an HTTPS URL pointing to an SVG item (max 5MB):
|
||||
|
||||
```bash
|
||||
scalingo env-set CUSTOM_LOGO_URL="https://cdn.yourdomain.com/logo.svg"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Logs
|
||||
|
||||
```bash
|
||||
scalingo logs --tail
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Build fails**: Check that all required environment variables are set
|
||||
2. **Database connection error**: Verify `DATABASE_URL` is correctly set to `$SCALINGO_POSTGRESQL_URL`
|
||||
3. **Static files not served**: Ensure the buildpack post-frontend script ran successfully
|
||||
4. **OIDC errors**: Verify your OIDC provider configuration and callback URLs
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# Open a console
|
||||
scalingo run bash
|
||||
|
||||
# Restart the app
|
||||
scalingo restart
|
||||
|
||||
# Scale containers
|
||||
scalingo scale web:2
|
||||
|
||||
# One-off command
|
||||
scalingo run python manage.py shell
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
On Scalingo, the application runs as follows:
|
||||
|
||||
1. **Build Phase**: The buildpack compiles both frontend and backend
|
||||
2. **Runtime**:
|
||||
- Nginx serves static files and proxies to the backend
|
||||
- Gunicorn runs the Django WSGI application
|
||||
- Both processes are managed by the `bin/buildpack_start.sh` script
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Scalingo Documentation](https://doc.scalingo.com/)
|
||||
- [Suite Numérique Buildpack](https://github.com/suitenumerique/buildpack)
|
||||
- [Meet Environment Variables](../../src/helm/meet/README.md)
|
||||
- [Django Configurations Documentation](https://django-configurations.readthedocs.io/)
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.5.0"
|
||||
version = "1.7.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.3.10",
|
||||
|
||||
@@ -115,6 +115,10 @@ class RoomAdmin(admin.ModelAdmin):
|
||||
list_filter = ["access_level", "created_at"]
|
||||
readonly_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Optimize queries by prefetching related access and user data to avoid N+1 queries."""
|
||||
return super().get_queryset(request).prefetch_related("accesses__user")
|
||||
|
||||
def get_owner(self, obj):
|
||||
"""Return the owner of the room for display in the admin list."""
|
||||
|
||||
@@ -138,6 +142,7 @@ class RecordingAccessInline(admin.TabularInline):
|
||||
|
||||
model = models.RecordingAccess
|
||||
extra = 0
|
||||
autocomplete_fields = ["user"]
|
||||
|
||||
|
||||
@admin.action(description=_("Resend notification to external service"))
|
||||
@@ -207,8 +212,18 @@ class RecordingAdmin(admin.ModelAdmin):
|
||||
"created_at",
|
||||
"worker_id",
|
||||
)
|
||||
list_filter = ["status", "room", "created_at"]
|
||||
readonly_fields = ["id", "created_at", "updated_at"]
|
||||
list_filter = ["created_at"]
|
||||
list_select_related = ("room",)
|
||||
readonly_fields = (
|
||||
"id",
|
||||
"created_at",
|
||||
"options",
|
||||
"mode",
|
||||
"room",
|
||||
"status",
|
||||
"updated_at",
|
||||
"worker_id",
|
||||
)
|
||||
actions = [resend_notification]
|
||||
|
||||
def get_queryset(self, request):
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Throttling modules for the API."""
|
||||
|
||||
from lasuite.drf.throttling import MonitoredThrottleMixin
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
from sentry_sdk import capture_message
|
||||
|
||||
|
||||
def sentry_monitoring_throttle_failure(message):
|
||||
"""Log when a failure occurs to detect rate limiting issues."""
|
||||
capture_message(message, "warning")
|
||||
|
||||
|
||||
class MonitoredAnonRateThrottle(MonitoredThrottleMixin, AnonRateThrottle):
|
||||
"""Throttle for the monitored scoped rate throttle."""
|
||||
|
||||
|
||||
class RequestEntryAnonRateThrottle(MonitoredAnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room entry"""
|
||||
|
||||
scope = "request_entry"
|
||||
|
||||
|
||||
class CreationCallbackAnonRateThrottle(MonitoredAnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room generation callback"""
|
||||
|
||||
scope = "creation_callback"
|
||||
@@ -10,7 +10,7 @@ from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.text import slugify
|
||||
|
||||
from rest_framework import decorators, mixins, pagination, throttling, viewsets
|
||||
from rest_framework import decorators, mixins, pagination, viewsets
|
||||
from rest_framework import (
|
||||
exceptions as drf_exceptions,
|
||||
)
|
||||
@@ -58,7 +58,7 @@ from core.services.room_creation import RoomCreation
|
||||
from core.services.subtitle import SubtitleException, SubtitleService
|
||||
|
||||
from ..authentication.livekit import LiveKitTokenAuthentication
|
||||
from . import permissions, serializers
|
||||
from . import permissions, serializers, throttling
|
||||
from .feature_flag import FeatureFlag
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
@@ -191,18 +191,6 @@ class UserViewSet(
|
||||
)
|
||||
|
||||
|
||||
class RequestEntryAnonRateThrottle(throttling.AnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room entry"""
|
||||
|
||||
scope = "request_entry"
|
||||
|
||||
|
||||
class CreationCallbackAnonRateThrottle(throttling.AnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room generation callback"""
|
||||
|
||||
scope = "creation_callback"
|
||||
|
||||
|
||||
class RoomViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
@@ -379,7 +367,7 @@ class RoomViewSet(
|
||||
methods=["post"],
|
||||
url_path="request-entry",
|
||||
permission_classes=[],
|
||||
throttle_classes=[RequestEntryAnonRateThrottle],
|
||||
throttle_classes=[throttling.RequestEntryAnonRateThrottle],
|
||||
)
|
||||
def request_entry(self, request, pk=None): # pylint: disable=unused-argument
|
||||
"""Request entry to a room"""
|
||||
@@ -489,7 +477,7 @@ class RoomViewSet(
|
||||
methods=["post"],
|
||||
url_path="creation-callback",
|
||||
permission_classes=[],
|
||||
throttle_classes=[CreationCallbackAnonRateThrottle],
|
||||
throttle_classes=[throttling.CreationCallbackAnonRateThrottle],
|
||||
)
|
||||
def creation_callback(self, request):
|
||||
"""Retrieve cached room data via an unauthenticated request with a unique ID.
|
||||
|
||||
@@ -10,6 +10,8 @@ import jwt as pyJwt
|
||||
from lasuite.oidc_resource_server.backend import ResourceServerBackend as LaSuiteBackend
|
||||
from rest_framework import authentication, exceptions
|
||||
|
||||
from core.models import Application
|
||||
|
||||
User = get_user_model()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -94,6 +96,18 @@ class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
logger.warning("Missing 'client_id' in JWT payload")
|
||||
raise exceptions.AuthenticationFailed("Invalid token claims.")
|
||||
|
||||
try:
|
||||
application = Application.objects.get(client_id=client_id)
|
||||
except Application.DoesNotExist as e:
|
||||
logger.warning("Application not found: %s", client_id)
|
||||
raise exceptions.AuthenticationFailed("Application not found.") from e
|
||||
|
||||
if not application.active:
|
||||
logger.warning(
|
||||
"Inactive application attempted authentication: %s", client_id
|
||||
)
|
||||
raise exceptions.AuthenticationFailed("Application is disabled.")
|
||||
|
||||
if not is_delegated:
|
||||
logger.warning("Token is not marked as delegated")
|
||||
raise exceptions.AuthenticationFailed("Invalid token type.")
|
||||
|
||||
@@ -33,12 +33,11 @@ class BaseScopePermission(permissions.BasePermission):
|
||||
Raises:
|
||||
PermissionDenied: If required scope is missing from token
|
||||
"""
|
||||
# Get the current action (e.g., 'list', 'create')
|
||||
# Get the current action (e.g., 'list', 'create'), if None let DRF handle it
|
||||
action = getattr(view, "action", None)
|
||||
if not action:
|
||||
raise exceptions.PermissionDenied(
|
||||
"Insufficient permissions. Unknown action."
|
||||
)
|
||||
# DRF routers return a 405 for unsupported methods
|
||||
return True
|
||||
|
||||
required_scope = self.scope_map.get(action)
|
||||
if not required_scope:
|
||||
@@ -57,9 +56,12 @@ class BaseScopePermission(permissions.BasePermission):
|
||||
if isinstance(token_scopes, str):
|
||||
token_scopes = token_scopes.split()
|
||||
|
||||
# Ensure scopes is a deduplicated list (preserving order) and lowercase all scopes
|
||||
token_scopes = list(dict.fromkeys(scope.lower() for scope in token_scopes))
|
||||
|
||||
if settings.OIDC_RS_SCOPES_PREFIX:
|
||||
token_scopes = [
|
||||
scope.replace(f"{settings.OIDC_RS_SCOPES_PREFIX}:", "")
|
||||
scope.removeprefix(f"{settings.OIDC_RS_SCOPES_PREFIX}:")
|
||||
for scope in token_scopes
|
||||
]
|
||||
|
||||
@@ -82,3 +84,23 @@ class HasRequiredRoomScope(BaseScopePermission):
|
||||
"partial_update": models.ApplicationScope.ROOMS_UPDATE,
|
||||
"destroy": models.ApplicationScope.ROOMS_DELETE,
|
||||
}
|
||||
|
||||
|
||||
class RoomPermissions(permissions.BasePermission):
|
||||
"""Permissions applying to the room API endpoint."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Allow access only to authenticated users."""
|
||||
return request.user.is_authenticated
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""Enforce role-based access: read=any role, delete=owner, write=admin or owner."""
|
||||
user = request.user
|
||||
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return obj.has_any_role(user)
|
||||
|
||||
if request.method == "DELETE":
|
||||
return obj.is_owner(user)
|
||||
|
||||
return obj.is_administrator_or_owner(user)
|
||||
|
||||
@@ -182,7 +182,9 @@ class RoomViewSet(
|
||||
ResourceServerAuthentication,
|
||||
]
|
||||
permission_classes = [
|
||||
api.permissions.IsAuthenticated & permissions.HasRequiredRoomScope
|
||||
api.permissions.IsAuthenticated
|
||||
& permissions.HasRequiredRoomScope
|
||||
& permissions.RoomPermissions
|
||||
]
|
||||
queryset = models.Room.objects.all()
|
||||
serializer_class = serializers.RoomSerializer
|
||||
|
||||
@@ -292,6 +292,10 @@ class Resource(BaseModel):
|
||||
role = RoleChoices.MEMBER
|
||||
return role
|
||||
|
||||
def has_any_role(self, user):
|
||||
"""Check if a user has any role on the resource."""
|
||||
return self.get_role(user) is not None
|
||||
|
||||
def is_administrator_or_owner(self, user):
|
||||
"""
|
||||
Check if a user is administrator or owner of the resource."""
|
||||
|
||||
@@ -102,6 +102,7 @@ def test_notify_user_by_email_success(mocked_current_site, settings):
|
||||
settings.EMAIL_SUPPORT_EMAIL = "support@acme.com"
|
||||
settings.EMAIL_LOGO_IMG = "https://acme.com/logo"
|
||||
settings.SCREEN_RECORDING_BASE_URL = "https://acme.com/recordings"
|
||||
settings.RECORDING_DOWNLOAD_BASE_URL = None
|
||||
settings.EMAIL_FROM = "notifications@acme.com"
|
||||
|
||||
recording = factories.RecordingFactory(room__name="Conference Room A")
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
Tests for external API /room endpoint
|
||||
"""
|
||||
|
||||
# pylint: disable=W0621
|
||||
# pylint: disable=W0621,C0302
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
import responses
|
||||
from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core.factories import (
|
||||
RoomFactory,
|
||||
UserFactory,
|
||||
)
|
||||
from core.factories import ApplicationFactory, RoomFactory, UserFactory
|
||||
from core.models import ApplicationScope, RoleChoices, Room, RoomAccessLevel, User
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
@@ -27,12 +27,14 @@ def generate_test_token(user, scopes):
|
||||
now = datetime.now(timezone.utc)
|
||||
scope_string = " ".join(scopes)
|
||||
|
||||
application = ApplicationFactory()
|
||||
|
||||
payload = {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(seconds=settings.APPLICATION_JWT_EXPIRATION_SECONDS),
|
||||
"client_id": "test-client-id",
|
||||
"client_id": str(application.client_id),
|
||||
"scope": scope_string,
|
||||
"user_id": str(user.id),
|
||||
"delegated": True,
|
||||
@@ -53,6 +55,23 @@ def test_api_rooms_list_requires_authentication():
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_api_rooms_list_inactive_user(settings):
|
||||
"""List should return 401 if user is inactive."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user1 = UserFactory(is_active=False)
|
||||
RoomFactory(users=[(user1, RoleChoices.OWNER)])
|
||||
|
||||
token = generate_test_token(user1, [ApplicationScope.ROOMS_LIST])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "user account is disabled" in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_list_with_valid_token(settings):
|
||||
"""Listing rooms with valid token should succeed."""
|
||||
|
||||
@@ -73,6 +92,24 @@ def test_api_rooms_list_with_valid_token(settings):
|
||||
assert response.data["results"][0]["id"] == str(room.id)
|
||||
|
||||
|
||||
def test_api_rooms_list_with_no_rooms(settings):
|
||||
"""Listing rooms with a valid token returns an empty list when there are no rooms."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
# Generate valid token
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_LIST])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data["count"] == 0
|
||||
assert response.data["results"] == []
|
||||
|
||||
|
||||
def test_api_rooms_list_with_expired_token(settings):
|
||||
"""Listing rooms with expired token should return 401."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
@@ -92,8 +129,8 @@ def test_api_rooms_list_with_expired_token(settings):
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_rooms_list_with_invalid_token(settings):
|
||||
"""Listing rooms with invalid token should return 400."""
|
||||
def test_api_rooms_list_with_invalid_rs_token(settings):
|
||||
"""Listing rooms with invalid resource server token should return 400."""
|
||||
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
@@ -130,7 +167,27 @@ def test_api_rooms_list_missing_scope(settings):
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert "Insufficient permissions. Required scope: rooms:list" in str(response.data)
|
||||
assert (
|
||||
"insufficient permissions. required scope: rooms:list"
|
||||
in str(response.data).lower()
|
||||
)
|
||||
|
||||
|
||||
def test_api_rooms_list_no_scope(settings):
|
||||
"""Listing rooms without any scope should return 403."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
# Token without scope
|
||||
token = generate_test_token(user, [])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert "insufficient permissions." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_list_filters_by_user(settings):
|
||||
@@ -144,7 +201,9 @@ def test_api_rooms_list_filters_by_user(settings):
|
||||
room2 = RoomFactory(users=[(user2, RoleChoices.OWNER)])
|
||||
room3 = RoomFactory(users=[(user1, RoleChoices.MEMBER)])
|
||||
|
||||
token = generate_test_token(user1, [ApplicationScope.ROOMS_LIST])
|
||||
token = generate_test_token(
|
||||
user1, [ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE]
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
@@ -158,6 +217,84 @@ def test_api_rooms_list_filters_by_user(settings):
|
||||
assert str(room2.id) not in returned_ids
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_requires_authentication(settings):
|
||||
"""Retrieving rooms without authentication should return 401."""
|
||||
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user1 = UserFactory()
|
||||
room1 = RoomFactory(users=[(user1, RoleChoices.OWNER)])
|
||||
|
||||
client = APIClient()
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room1.id}/")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_inactive_user(settings):
|
||||
"""Retrieve should return 401 if user is inactive."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user1 = UserFactory(is_active=False)
|
||||
room1 = RoomFactory(users=[(user1, RoleChoices.OWNER)])
|
||||
|
||||
token = generate_test_token(user1, [ApplicationScope.ROOMS_LIST])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room1.id}/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "user account is disabled" in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_with_expired_token(settings):
|
||||
"""Retrieving rooms with expired token should return 401."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
settings.APPLICATION_JWT_EXPIRATION_SECONDS = 0
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
# Generate expired token
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_CREATE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "expired" in str(response.data).lower()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_rooms_retrieve_with_invalid_rs_token(settings):
|
||||
"""Retrieving rooms with invalid resource server token should return 400."""
|
||||
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={
|
||||
"iss": "https://oidc.example.com",
|
||||
"active": False,
|
||||
},
|
||||
)
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer invalid-token-123")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
# Return 400 instead of 401 because ResourceServerAuthentication raises
|
||||
# SuspiciousOperation when the introspected user is not active
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_requires_scope(settings):
|
||||
"""Retrieving a room requires ROOMS_RETRIEVE scope."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
@@ -178,6 +315,24 @@ def test_api_rooms_retrieve_requires_scope(settings):
|
||||
)
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_no_scope(settings):
|
||||
"""Retrieving rooms without any scope should return 403."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
# Token without scope
|
||||
token = generate_test_token(user, [])
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert "insufficient permissions." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_success(settings):
|
||||
"""Retrieving a room with correct scope should succeed."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
@@ -212,6 +367,132 @@ def test_api_rooms_retrieve_success(settings):
|
||||
}
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_success_by_user(settings):
|
||||
"""Retrieve should only return rooms accessible to the authenticated user."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user1 = UserFactory()
|
||||
user2 = UserFactory()
|
||||
|
||||
room1 = RoomFactory(users=[(user1, RoleChoices.OWNER)])
|
||||
room2 = RoomFactory(users=[(user2, RoleChoices.OWNER)])
|
||||
room3 = RoomFactory(users=[(user1, RoleChoices.MEMBER)])
|
||||
room4 = RoomFactory(users=[(user1, RoleChoices.ADMIN)])
|
||||
|
||||
token = generate_test_token(
|
||||
user1, [ApplicationScope.ROOMS_RETRIEVE, ApplicationScope.ROOMS_LIST]
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room2.id}/")
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room1.id}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room3.id}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room4.id}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_not_found(settings):
|
||||
"""Retrieving a non-existing room with correct scope should return a 404."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_RETRIEVE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{uuid.uuid4()}/")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "no room matches the given query." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_create_requires_authentication(settings):
|
||||
"""Creating rooms without authentication should return 401."""
|
||||
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
client = APIClient()
|
||||
response = client.post("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_api_rooms_create_with_expired_token(settings):
|
||||
"""Creating rooms with expired token should return 401."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
settings.APPLICATION_JWT_EXPIRATION_SECONDS = 0
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
# Generate expired token
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_CREATE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "expired" in str(response.data).lower()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_rooms_create_with_invalid_rs_token(settings):
|
||||
"""Creating rooms with invalid resource server token should return 400."""
|
||||
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={
|
||||
"iss": "https://oidc.example.com",
|
||||
"active": False,
|
||||
},
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer invalid-token-123")
|
||||
response = client.post("/external-api/v1.0/rooms/")
|
||||
|
||||
# Return 400 instead of 401 because ResourceServerAuthentication raises
|
||||
# SuspiciousOperation when the introspected user is not active
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_api_rooms_create_inactive_user(settings):
|
||||
"""Create should return 401 if user is inactive."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user1 = UserFactory(is_active=False)
|
||||
|
||||
token = generate_test_token(user1, [ApplicationScope.ROOMS_CREATE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "user account is disabled" in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_create_requires_scope(settings):
|
||||
"""Creating a room requires ROOMS_CREATE scope."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
@@ -225,18 +506,38 @@ def test_api_rooms_create_requires_scope(settings):
|
||||
response = client.post("/external-api/v1.0/rooms/", {}, format="json")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert "Insufficient permissions. Required scope: rooms:create" in str(
|
||||
response.data
|
||||
assert (
|
||||
"insufficient permissions. required scope: rooms:create"
|
||||
in str(response.data).lower()
|
||||
)
|
||||
|
||||
|
||||
def test_api_rooms_create_no_scope(settings):
|
||||
"""Creating rooms without any scope should return 403."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
# Token without scope
|
||||
token = generate_test_token(user, [])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert "insufficient permissions." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_create_success(settings):
|
||||
"""Creating a room with correct scope should succeed."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_CREATE])
|
||||
token = generate_test_token(
|
||||
user, [ApplicationScope.ROOMS_CREATE, ApplicationScope.ROOMS_LIST]
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
@@ -245,6 +546,8 @@ def test_api_rooms_create_success(settings):
|
||||
assert response.status_code == 201
|
||||
assert "id" in response.data
|
||||
assert "slug" in response.data
|
||||
assert "name" in response.data
|
||||
assert response.data["name"] == response.data["slug"]
|
||||
|
||||
# Verify room was created with user as owner
|
||||
room = Room.objects.get(id=response.data["id"])
|
||||
@@ -252,6 +555,72 @@ def test_api_rooms_create_success(settings):
|
||||
assert room.access_level == "trusted"
|
||||
|
||||
|
||||
def test_api_rooms_create_readonly_enforcement(settings):
|
||||
"""Creating a room succeeds and any provided read-only fields are ignored."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_CREATE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post(
|
||||
"/external-api/v1.0/rooms/",
|
||||
{
|
||||
"id": "fake-id",
|
||||
"slug": "fake-slug",
|
||||
"name": "fake-name",
|
||||
"access_level": "public",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert "slug" in response.data
|
||||
assert response.data["id"] != "fake-id"
|
||||
assert "name" in response.data
|
||||
assert response.data["slug"] != "fake-slug"
|
||||
assert "id" in response.data
|
||||
assert response.data["name"] != "fake-name"
|
||||
|
||||
# Verify room was created with user as owner
|
||||
room = Room.objects.get(id=response.data["id"])
|
||||
assert room.get_role(user) == RoleChoices.OWNER
|
||||
assert room.access_level == "trusted"
|
||||
|
||||
|
||||
def test_api_rooms_unknown_actions(settings):
|
||||
"""Updating or deleting a room are not supported yet."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
token = generate_test_token(
|
||||
user,
|
||||
[
|
||||
ApplicationScope.ROOMS_RETRIEVE,
|
||||
ApplicationScope.ROOMS_DELETE,
|
||||
ApplicationScope.ROOMS_UPDATE,
|
||||
],
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 405
|
||||
assert 'method "delete" not allowed.' in str(response.data).lower()
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.patch(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 405
|
||||
assert 'method "patch" not allowed.' in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_response_no_url(settings):
|
||||
"""Response should not include url field when APPLICATION_BASE_URL is None."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
@@ -290,10 +659,43 @@ def test_api_rooms_response_no_telephony(settings):
|
||||
assert response.data["id"] == str(room.id)
|
||||
|
||||
|
||||
def test_api_rooms_token_scope_case_insensitive(settings):
|
||||
"""Token's scope should be case-insensitive."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory()
|
||||
application = ApplicationFactory()
|
||||
|
||||
# Generate token with mixed-case scope "Rooms:List" to verify that scope
|
||||
# validation is case-insensitive (should match "rooms:list")
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(hours=1),
|
||||
"client_id": str(application.client_id),
|
||||
"scope": "Rooms:List", # Mixed case - should be accepted as "rooms:list"
|
||||
"user_id": str(user.id),
|
||||
"delegated": True,
|
||||
}
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithm=settings.APPLICATION_JWT_ALG,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_api_rooms_token_without_delegated_flag(settings):
|
||||
"""Token without delegated flag should be rejected."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory()
|
||||
application = ApplicationFactory()
|
||||
|
||||
# Generate token without delegated flag
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -302,7 +704,7 @@ def test_api_rooms_token_without_delegated_flag(settings):
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(hours=1),
|
||||
"client_id": "test-client",
|
||||
"client_id": str(application.client_id),
|
||||
"scope": "rooms:list",
|
||||
"user_id": str(user.id),
|
||||
"delegated": False, # Not delegated
|
||||
@@ -318,7 +720,73 @@ def test_api_rooms_token_without_delegated_flag(settings):
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "Invalid token type." in str(response.data)
|
||||
assert "invalid token type." in str(response.data).lower()
|
||||
|
||||
|
||||
@mock.patch.object(ResourceServerAuthentication, "authenticate", return_value=None)
|
||||
def test_api_rooms_token_invalid_signature(mock_rs_authenticate, settings):
|
||||
"""Token signed with an invalid key should defer to the next authentication."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory()
|
||||
application = ApplicationFactory()
|
||||
|
||||
# Generate token without delegated flag
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(hours=1),
|
||||
"client_id": str(application.client_id),
|
||||
"scope": "rooms:list",
|
||||
"user_id": str(user.id),
|
||||
"delegated": True,
|
||||
}
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
"invalid-private-key",
|
||||
algorithm=settings.APPLICATION_JWT_ALG,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
mock_rs_authenticate.assert_called()
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@mock.patch.object(ResourceServerAuthentication, "authenticate", return_value=None)
|
||||
def test_api_rooms_token_invalid_alg(mock_rs_authenticate, settings):
|
||||
"""Token signed with an invalid alg should defer to the next authentication."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
settings.APPLICATION_JWT_ALG = "RS256"
|
||||
user = UserFactory()
|
||||
|
||||
# Generate token without delegated flag
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(hours=1),
|
||||
"client_id": "test-client",
|
||||
"scope": "rooms:list",
|
||||
"user_id": str(user.id),
|
||||
"delegated": True,
|
||||
}
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithm="HS256", # different value
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
mock_rs_authenticate.assert_called()
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_api_rooms_token_missing_client_id(settings):
|
||||
@@ -348,7 +816,157 @@ def test_api_rooms_token_missing_client_id(settings):
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "Invalid token claims." in str(response.data)
|
||||
assert "invalid token claims." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_token_missing_user_id(settings):
|
||||
"""Token without user_id should be rejected."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
application = ApplicationFactory()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(hours=1),
|
||||
"client_id": str(application.client_id),
|
||||
"scope": "rooms:list",
|
||||
"delegated": True,
|
||||
# Missing user_id
|
||||
}
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithm=settings.APPLICATION_JWT_ALG,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "invalid token claims." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_token_invalid_audience(settings):
|
||||
"""Token with an invalid audience should be rejected."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory()
|
||||
application = ApplicationFactory()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": "invalid-audience",
|
||||
"iat": now,
|
||||
"exp": now + timedelta(hours=1),
|
||||
"client_id": str(application.client_id),
|
||||
"user_id": str(user.id),
|
||||
"scope": "rooms:list",
|
||||
"delegated": True,
|
||||
}
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithm=settings.APPLICATION_JWT_ALG,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "invalid token." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_token_unknown_user(settings):
|
||||
"""Token for unknown user should be rejected."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
application = ApplicationFactory()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(hours=1),
|
||||
"client_id": str(application.client_id),
|
||||
"user_id": str(uuid.uuid4()),
|
||||
"scope": "rooms:list",
|
||||
"delegated": True,
|
||||
}
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithm=settings.APPLICATION_JWT_ALG,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "user not found." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_token_unknown_application(settings):
|
||||
"""Token for unknown application should be rejected."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(hours=1),
|
||||
"client_id": "unknown-client-id",
|
||||
"user_id": str(uuid.uuid4()),
|
||||
"scope": "rooms:list",
|
||||
"delegated": True,
|
||||
}
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithm=settings.APPLICATION_JWT_ALG,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "application not found." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_token_inactive_application(settings):
|
||||
"""Token for inactive application should be rejected."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
application = ApplicationFactory(active=False)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(hours=1),
|
||||
"client_id": str(application.client_id),
|
||||
"user_id": str(uuid.uuid4()),
|
||||
"scope": "rooms:list",
|
||||
"delegated": True,
|
||||
}
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithm=settings.APPLICATION_JWT_ALG,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "application is disabled." in str(response.data).lower()
|
||||
|
||||
|
||||
@responses.activate
|
||||
@@ -507,7 +1125,7 @@ def test_resource_server_authentication_successful(settings):
|
||||
"aud": "some_client_id", # settings.OIDC_RS_CLIENT_ID
|
||||
"sub": "very-specific-sub",
|
||||
"client_id": "some_service_provider",
|
||||
"scope": "openid lasuite_meet lasuite_meet:rooms:list",
|
||||
"scope": "openid lasuite_meet lasuite_meet:rooms:list lasuite_meet:rooms:retrieve",
|
||||
"active": True,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ from socket import gethostbyname, gethostname
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import dj_database_url
|
||||
import sentry_sdk
|
||||
from configurations import Configuration, values
|
||||
from lasuite.configuration.values import SecretFileValue
|
||||
@@ -92,7 +93,11 @@ class Base(Configuration):
|
||||
|
||||
# Database
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"default": dj_database_url.config()
|
||||
if values.DatabaseURLValue(
|
||||
None, environ_name="DATABASE_URL", environ_prefix=None
|
||||
)
|
||||
else {
|
||||
"ENGINE": values.Value(
|
||||
"django.db.backends.postgresql_psycopg2",
|
||||
environ_name="DB_ENGINE",
|
||||
@@ -292,6 +297,9 @@ class Base(Configuration):
|
||||
),
|
||||
},
|
||||
}
|
||||
MONITORED_THROTTLE_FAILURE_CALLBACK = (
|
||||
"core.api.throttling.sentry_monitoring_throttle_failure"
|
||||
)
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "Meet API",
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.5.0"
|
||||
version = "1.7.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -29,6 +29,7 @@ dependencies = [
|
||||
"Brotli==1.2.0",
|
||||
"brevo-python==1.2.0",
|
||||
"celery[redis]==5.5.3",
|
||||
"dj-database-url==3.1.0",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.9.0",
|
||||
"django-countries==8.0.0",
|
||||
@@ -38,7 +39,7 @@ dependencies = [
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.2.9",
|
||||
"django==5.2.11",
|
||||
"djangorestframework==3.16.1",
|
||||
"drf_spectacular==0.29.0",
|
||||
"dockerflow==2024.4.2",
|
||||
|
||||
@@ -5,6 +5,12 @@ server {
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location = /.wellknown/windows-app-web-link {
|
||||
default_type application/json;
|
||||
alias /usr/share/nginx/html/.wellknown/windows-app-web-link;
|
||||
add_header Content-Disposition "attachment; filename=windows-app-web-link";
|
||||
}
|
||||
|
||||
# Serve static files with caching
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
|
||||
Generated
+515
-154
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.30",
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.34",
|
||||
"@fontsource/material-icons-outlined": "5.2.6",
|
||||
"@livekit/components-react": "2.9.13",
|
||||
"@livekit/components-styles": "1.1.6",
|
||||
"@livekit/track-processors": "0.6.1",
|
||||
"@pandacss/preset-panda": "0.54.0",
|
||||
"@react-aria/toast": "3.0.5",
|
||||
"@react-types/overlays": "3.9.0",
|
||||
"@react-aria/toast": "3.0.10",
|
||||
"@react-types/overlays": "3.9.3",
|
||||
"@remixicon/react": "4.6.0",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"crisp-sdk-web": "1.0.27",
|
||||
"derive-valtio": "0.2.0",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.0",
|
||||
"i18next": "25.3.1",
|
||||
"humanize-duration": "3.33.2",
|
||||
"i18next": "25.8.4",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"i18next-parser": "9.3.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"libphonenumber-js": "1.12.10",
|
||||
"livekit-client": "2.15.7",
|
||||
"posthog-js": "1.256.2",
|
||||
"posthog-js": "1.342.1",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.10.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "15.1.1",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.1.5",
|
||||
"wouter": "3.7.1"
|
||||
"valtio": "2.3.0",
|
||||
"wouter": "3.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pandacss/dev": "0.54.0",
|
||||
@@ -55,7 +55,7 @@
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.6.2",
|
||||
"prettier": "3.8.1",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.0.8",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
@@ -363,9 +363,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1021,9 +1021,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fontsource-variable/material-symbols-outlined": {
|
||||
"version": "5.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/material-symbols-outlined/-/material-symbols-outlined-5.2.30.tgz",
|
||||
"integrity": "sha512-BjSx7nqvISJs2Pjd8sBH583AnD4k6dD4Em7AVISoLXzbX3PIFWAE2GPm13LlCys8u8idkyUd62L8yn6ts6DdbA==",
|
||||
"version": "5.2.34",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/material-symbols-outlined/-/material-symbols-outlined-5.2.34.tgz",
|
||||
"integrity": "sha512-FfJVoKdYlxvIHflVZlrevf6e1WmbJGeAdbcLmOmkftoP2Du0qrRD/SmNchIXijsKXF7j3z1m++e92Qi6/U+jRA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
@@ -1158,9 +1158,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@internationalized/date": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.2.tgz",
|
||||
"integrity": "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==",
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.11.0.tgz",
|
||||
"integrity": "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
@@ -1177,9 +1177,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@internationalized/number": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.3.tgz",
|
||||
"integrity": "sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==",
|
||||
"version": "3.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz",
|
||||
"integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
@@ -1354,6 +1354,252 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api-logs": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
|
||||
"integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/core": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
|
||||
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/exporter-logs-otlp-http": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz",
|
||||
"integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.208.0",
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/otlp-exporter-base": "0.208.0",
|
||||
"@opentelemetry/otlp-transformer": "0.208.0",
|
||||
"@opentelemetry/sdk-logs": "0.208.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/otlp-exporter-base": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
|
||||
"integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/otlp-transformer": "0.208.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/otlp-transformer": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
|
||||
"integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.208.0",
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0",
|
||||
"@opentelemetry/sdk-logs": "0.208.0",
|
||||
"@opentelemetry/sdk-metrics": "2.2.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.2.0",
|
||||
"protobufjs": "^7.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/resources": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz",
|
||||
"integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz",
|
||||
"integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-logs": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
|
||||
"integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.208.0",
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.4.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-metrics": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
|
||||
"integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.9.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-trace-base": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
|
||||
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/semantic-conventions": {
|
||||
"version": "1.39.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz",
|
||||
"integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@pandacss/config": {
|
||||
"version": "0.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@pandacss/config/-/config-0.54.0.tgz",
|
||||
@@ -1746,6 +1992,85 @@
|
||||
"resolved": "https://registry.npmjs.org/@pandacss/types/-/types-0.54.0.tgz",
|
||||
"integrity": "sha512-5kspg2UOgFWrawbHeoleZvbZ6id/kIBLHoDeD1CnLbl9fEbZAZ3Avi5Hi1mIFe9E8PFzp9V+N9IfQL7pYhavfA=="
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.20.1.tgz",
|
||||
"integrity": "sha512-uoTmWkYCtLYFpiK37/JCq+BuCA/OZn1qQZn5cPv1EEKt3ni3Zgg48xWCnSEyGFl5KKSXlfCruiRTwnbAtCgrBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/types": {
|
||||
"version": "1.342.1",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.342.1.tgz",
|
||||
"integrity": "sha512-bcyBdO88FWTkd5AVTa4Nu8T7RfY0WJrG7WMCXum/rcvNjYhS3DmOfKf8o/Bt56vA3J3yeU0vbgrmltYVoTAfaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@react-aria/autocomplete": {
|
||||
"version": "3.0.0-beta.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/autocomplete/-/autocomplete-3.0.0-beta.5.tgz",
|
||||
@@ -2096,18 +2421,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/i18n": {
|
||||
"version": "3.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.10.tgz",
|
||||
"integrity": "sha512-1j00soQ2W0nTgzaaIsGFdMF/5aN60AEdCJPhmXGZiuWdWzMxObN9LQ9vdzYPTjTqyqMdSaSp9DZKs5I26Xovpw==",
|
||||
"version": "3.12.15",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.15.tgz",
|
||||
"integrity": "sha512-3CrAN7ORVHrckvTmbPq76jFZabqq+rScosGT5+ElircJ5rF5+JcdT99Hp5Xg6R10jk74e8G3xiqdYsUd+7iJMA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@internationalized/date": "^3.8.2",
|
||||
"@internationalized/date": "^3.11.0",
|
||||
"@internationalized/message": "^3.1.8",
|
||||
"@internationalized/number": "^3.6.3",
|
||||
"@internationalized/number": "^3.6.5",
|
||||
"@internationalized/string": "^3.2.7",
|
||||
"@react-aria/ssr": "^3.9.9",
|
||||
"@react-aria/utils": "^3.29.1",
|
||||
"@react-types/shared": "^3.30.0",
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-aria/utils": "^3.33.0",
|
||||
"@react-types/shared": "^3.33.0",
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -2116,15 +2441,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/interactions": {
|
||||
"version": "3.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.3.tgz",
|
||||
"integrity": "sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==",
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.27.0.tgz",
|
||||
"integrity": "sha512-D27pOy+0jIfHK60BB26AgqjjRFOYdvVSkwC31b2LicIzRCSPOSP06V4gMHuGmkhNTF4+YWDi1HHYjxIvMeiSlA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.9",
|
||||
"@react-aria/utils": "^3.29.1",
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-aria/utils": "^3.33.0",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-types/shared": "^3.30.0",
|
||||
"@react-types/shared": "^3.33.0",
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -2148,15 +2473,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/landmark": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/landmark/-/landmark-3.0.4.tgz",
|
||||
"integrity": "sha512-1U5ce6cqg1qGbK4M4R6vwrhUrKXuUzReZwHaTrXxEY22IMxKDXIZL8G7pFpcKix2XKqjLZWf+g8ngGuNhtQ2QQ==",
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/landmark/-/landmark-3.0.9.tgz",
|
||||
"integrity": "sha512-YYyluDBCXupnMh91ccE5g27fczjYmzPebHqTkVYjH4B6k45pOoqsMmWBCMnOTl0qOCeioI+daT8W0MamAZzoSw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/utils": "^3.29.1",
|
||||
"@react-types/shared": "^3.30.0",
|
||||
"@react-aria/utils": "^3.33.0",
|
||||
"@react-types/shared": "^3.33.0",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
@@ -2457,9 +2782,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/ssr": {
|
||||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz",
|
||||
"integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==",
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
|
||||
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
@@ -2579,18 +2904,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/toast": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/toast/-/toast-3.0.5.tgz",
|
||||
"integrity": "sha512-uhwiZqPy6hqucBUL7z6uUZjAJ/ou3bNdTjZlXS+zbcm+T0dsjKDfzNkaebyZY7AX3cYkFCaRjc3N6omXwoAviw==",
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/toast/-/toast-3.0.10.tgz",
|
||||
"integrity": "sha512-irW5Cr4msbPo4A4ysjT70MDJbpGCe1h9SkFgdYXBPA4Xbi4jRT7TiEZeIS1I7Hsvp6shAK1Ld/m6NBS0b/gyzg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/i18n": "^3.12.10",
|
||||
"@react-aria/interactions": "^3.25.3",
|
||||
"@react-aria/landmark": "^3.0.4",
|
||||
"@react-aria/utils": "^3.29.1",
|
||||
"@react-stately/toast": "^3.1.1",
|
||||
"@react-types/button": "^3.12.2",
|
||||
"@react-types/shared": "^3.30.0",
|
||||
"@react-aria/i18n": "^3.12.15",
|
||||
"@react-aria/interactions": "^3.27.0",
|
||||
"@react-aria/landmark": "^3.0.9",
|
||||
"@react-aria/utils": "^3.33.0",
|
||||
"@react-stately/toast": "^3.1.3",
|
||||
"@react-types/button": "^3.15.0",
|
||||
"@react-types/shared": "^3.33.0",
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -2672,15 +2997,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/utils": {
|
||||
"version": "3.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.1.tgz",
|
||||
"integrity": "sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==",
|
||||
"version": "3.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.33.0.tgz",
|
||||
"integrity": "sha512-yvz7CMH8d2VjwbSa5nGXqjU031tYhD8ddax95VzJsHSPyqHDEGfxul8RkhGV6oO7bVqZxVs6xY66NIgae+FHjw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.9",
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-stately/utils": "^3.10.7",
|
||||
"@react-types/shared": "^3.30.0",
|
||||
"@react-stately/utils": "^3.11.0",
|
||||
"@react-types/shared": "^3.33.0",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
@@ -3112,13 +3437,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/toast": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/toast/-/toast-3.1.1.tgz",
|
||||
"integrity": "sha512-W4a6xcsFt/E+aHmR2eZK+/p7Y5rdyXSCQ5gKSnbck+S3lijEWAyV45Mv8v95CQqu0bQijj6sy2Js1szq10HVwg==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/toast/-/toast-3.1.3.tgz",
|
||||
"integrity": "sha512-mT9QJKmD523lqFpOp0VWZ6QHZENFK7HrodnNJDVc7g616s5GNmemdlkITV43fSY3tHeThCVvPu+Uzh7RvQ9mpQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
@@ -3170,9 +3495,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/utils": {
|
||||
"version": "3.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.7.tgz",
|
||||
"integrity": "sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==",
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz",
|
||||
"integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
@@ -3224,12 +3549,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-types/button": {
|
||||
"version": "3.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.12.2.tgz",
|
||||
"integrity": "sha512-QLoSCX8E7NFIdkVMa65TPieve0rKeltfcIxiMtrphjfNn+83L0IHMcbhjf4r4W19c/zqGbw3E53Hx8mNukoTUw==",
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.15.0.tgz",
|
||||
"integrity": "sha512-X/K2/Oeuq7Hi8nMIzx4/YlZuvWFiSOHZt27p4HmThCnNO/9IDFPmvPrpkYjWN5eN9Nuk+P5vZUb4A7QJgYpvGA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-types/shared": "^3.30.0"
|
||||
"@react-types/shared": "^3.33.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
@@ -3399,12 +3724,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-types/overlays": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.9.0.tgz",
|
||||
"integrity": "sha512-T2DqMcDN5p8vb4vu2igoLrAtuewaNImLS8jsK7th7OjwQZfIWJn5Y45jSxHtXJUddEg1LkUjXYPSXCMerMcULw==",
|
||||
"version": "3.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.9.3.tgz",
|
||||
"integrity": "sha512-LzetThNNk8T26pQRbs1I7+isuFhdFYREy7wJCsZmbB0FnZgCukGTfOtThZWv+ry11veyVJiX68jfl4SV6ACTWA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-types/shared": "^3.31.0"
|
||||
"@react-types/shared": "^3.33.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
@@ -3460,9 +3785,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-types/shared": {
|
||||
"version": "3.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.31.0.tgz",
|
||||
"integrity": "sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg==",
|
||||
"version": "3.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.33.0.tgz",
|
||||
"integrity": "sha512-xuUpP6MyuPmJtzNOqF5pzFUIHH2YogyOQfUQHag54PRmWB7AbjuGWBUv0l1UDmz6+AbzAYGmDVAzcRDOu2PFpw==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
@@ -4023,7 +4348,6 @@
|
||||
"version": "22.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.0.tgz",
|
||||
"integrity": "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@@ -4062,6 +4386,13 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz",
|
||||
"integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA=="
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
|
||||
@@ -5089,15 +5420,16 @@
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||
},
|
||||
"node_modules/crisp-sdk-web": {
|
||||
"version": "1.0.25",
|
||||
"resolved": "https://registry.npmjs.org/crisp-sdk-web/-/crisp-sdk-web-1.0.25.tgz",
|
||||
"integrity": "sha512-CWTHFFeHRV0oqiXoPh/aIAKhFs6xcIM4NenGPnClAMCZUDQgQsF1OWmZWmnVNjJriXUmWRgDfeUxcxygS0dCRA=="
|
||||
"version": "1.0.27",
|
||||
"resolved": "https://registry.npmjs.org/crisp-sdk-web/-/crisp-sdk-web-1.0.27.tgz",
|
||||
"integrity": "sha512-aNWR3te65YiaVFu/iwdqOo3cyUBZHUheE4d6EtgQu/T18jh/9SpoYXjXF/OzUD3Cqy0pGryoqtuy5gxD8tqX9Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dev": true,
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -5376,6 +5708,15 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||
@@ -6565,15 +6906,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-duration": {
|
||||
"version": "3.33.0",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.0.tgz",
|
||||
"integrity": "sha512-vYJX7BSzn7EQ4SaP2lPYVy+icHDppB6k7myNeI3wrSRfwMS5+BHyGgzpHR0ptqJ2AQ6UuIKrclSg5ve6Ci4IAQ==",
|
||||
"license": "Unlicense"
|
||||
"version": "3.33.2",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.2.tgz",
|
||||
"integrity": "sha512-K7Ny/ULO1hDm2nnhvAY+SJV1skxFb61fd073SG1IWJl+D44ULrruCuTyjHKjBVVcSuTlnY99DKtgEG39CM5QOQ==",
|
||||
"license": "Unlicense",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/EvanHahn"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.3.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.1.tgz",
|
||||
"integrity": "sha512-S4CPAx8LfMOnURnnJa8jFWvur+UX/LWcl6+61p9VV7SK2m0445JeBJ6tLD0D5SR0H29G4PYfWkEhivKG5p4RDg==",
|
||||
"version": "25.8.4",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.4.tgz",
|
||||
"integrity": "sha512-a9A0MnUjKvzjEN/26ZY1okpra9kA8MEwzYEz1BNm+IyxUKPRH6ihf0p7vj8YvULwZHKHl3zkJ6KOt4hewxBecQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -6590,7 +6934,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
@@ -7084,8 +7428,7 @@
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"node_modules/javascript-stringify": {
|
||||
"version": "2.1.0",
|
||||
@@ -7562,6 +7905,12 @@
|
||||
"url": "https://tidelift.com/funding/github/npm/loglevel"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/look-it-up": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/look-it-up/-/look-it-up-2.1.0.tgz",
|
||||
@@ -7739,7 +8088,7 @@
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -8043,7 +8392,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -8069,7 +8417,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@@ -8116,7 +8464,7 @@
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -8261,33 +8609,31 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.256.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.256.2.tgz",
|
||||
"integrity": "sha512-ypepnUHr33i5a1Uk39mozZXXTENRPC17HCG3WHKK6aRcpNwNs8uEqXaIKICGNM+qre+totKeTgl0WoaUFYmyoQ==",
|
||||
"version": "1.342.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.342.1.tgz",
|
||||
"integrity": "sha512-mMnQhWuKj4ejFicLtFzr52InmqploOyW1eInqXBkaVqE1DPhczBDmwsd9MSggY8kv0EXm8zgK+2tzBJUKcX5yg==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.208.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
|
||||
"@opentelemetry/resources": "^2.2.0",
|
||||
"@opentelemetry/sdk-logs": "^0.208.0",
|
||||
"@posthog/core": "1.20.1",
|
||||
"@posthog/types": "1.342.1",
|
||||
"core-js": "^3.38.1",
|
||||
"dompurify": "^3.3.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
"web-vitals": "^4.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rrweb/types": "2.0.0-alpha.17",
|
||||
"rrweb-snapshot": "2.0.0-alpha.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rrweb/types": {
|
||||
"optional": true
|
||||
},
|
||||
"rrweb-snapshot": {
|
||||
"optional": true
|
||||
}
|
||||
"preact": "^10.28.2",
|
||||
"query-selector-shadow-dom": "^1.0.1",
|
||||
"web-vitals": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.24.0",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.0.tgz",
|
||||
"integrity": "sha512-aK8Cf+jkfyuZ0ZZRG9FbYqwmEiGQ4y/PUO4SuTWoyWL244nZZh7bd5h2APd4rSNDYTBNghg1L+5iJN3Skxtbsw==",
|
||||
"version": "10.28.3",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz",
|
||||
"integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
@@ -8303,9 +8649,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -8331,6 +8677,30 @@
|
||||
"node": "10.* || >= 12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz",
|
||||
@@ -8346,6 +8716,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/query-selector-shadow-dom": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
|
||||
"integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -8747,17 +9123,6 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-snapshot": {
|
||||
"version": "2.0.0-alpha.17",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.17.tgz",
|
||||
"integrity": "sha512-GBg5pV8LHOTbeVmH2VHLEFR0mc2QpQMzAvcoxEGfPNWgWHc8UvKCyq7pqN1vA+fDZ+yXXbixeO0kB2pzVvFCBw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"postcss": "^8.4.38"
|
||||
}
|
||||
},
|
||||
"node_modules/rsvp": {
|
||||
"version": "4.8.5",
|
||||
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
|
||||
@@ -8915,7 +9280,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
@@ -8927,7 +9291,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -8993,7 +9356,7 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9540,7 +9903,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
@@ -9614,9 +9976,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
|
||||
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -9643,9 +10005,9 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/valtio": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.5.tgz",
|
||||
"integrity": "sha512-vsh1Ixu5mT0pJFZm+Jspvhga5GzHUTYv0/+Th203pLfh3/wbHwxhu/Z2OkZDXIgHfjnjBns7SN9HNcbDvPmaGw==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-2.3.0.tgz",
|
||||
"integrity": "sha512-1MfKNcmOIdBSatiJsYgw420n6jnD+jeoI0V+RkOQbCB0ElLh6GKUfPr0hc9uq/KBGeghivDEarRsKFFdSQQnKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"proxy-compare": "^3.0.1"
|
||||
@@ -9931,9 +10293,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/web-vitals": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
|
||||
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz",
|
||||
"integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webrtc-adapter": {
|
||||
@@ -9971,7 +10333,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
@@ -10037,9 +10398,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wouter": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/wouter/-/wouter-3.7.1.tgz",
|
||||
"integrity": "sha512-od5LGmndSUzntZkE2R5CHhoiJ7YMuTIbiXsa0Anytc2RATekgv4sfWRAxLEULBrp7ADzinWQw8g470lkT8+fOw==",
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/wouter/-/wouter-3.9.0.tgz",
|
||||
"integrity": "sha512-sF/od/PIgqEQBQcrN7a2x3MX6MQE6nW0ygCfy9hQuUkuB28wEZuu/6M5GyqkrrEu9M6jxdkgE12yDFsQMKos4Q==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -13,35 +13,35 @@
|
||||
"check": "prettier --check ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.30",
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.34",
|
||||
"@fontsource/material-icons-outlined": "5.2.6",
|
||||
"@livekit/components-react": "2.9.13",
|
||||
"@livekit/components-styles": "1.1.6",
|
||||
"@livekit/track-processors": "0.6.1",
|
||||
"@pandacss/preset-panda": "0.54.0",
|
||||
"@react-aria/toast": "3.0.5",
|
||||
"@react-types/overlays": "3.9.0",
|
||||
"@react-aria/toast": "3.0.10",
|
||||
"@react-types/overlays": "3.9.3",
|
||||
"@remixicon/react": "4.6.0",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"crisp-sdk-web": "1.0.27",
|
||||
"derive-valtio": "0.2.0",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.0",
|
||||
"i18next": "25.3.1",
|
||||
"humanize-duration": "3.33.2",
|
||||
"i18next": "25.8.4",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"i18next-parser": "9.3.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"libphonenumber-js": "1.12.10",
|
||||
"livekit-client": "2.15.7",
|
||||
"posthog-js": "1.256.2",
|
||||
"posthog-js": "1.342.1",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.10.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "15.1.1",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.1.5",
|
||||
"wouter": "3.7.1"
|
||||
"valtio": "2.3.0",
|
||||
"wouter": "3.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pandacss/dev": "0.54.0",
|
||||
@@ -60,7 +60,7 @@
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.6.2",
|
||||
"prettier": "3.8.1",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.0.8",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"packageFamilyName" : "Visio_g3z6ba6vek6vg",
|
||||
"paths" : [ "*" ]
|
||||
}
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Dialog, H, P, ScreenReaderAnnouncer } from '@/primitives'
|
||||
import { A, Button, Dialog, H, P, ScreenReaderAnnouncer } from '@/primitives'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useSnapshot } from 'valtio'
|
||||
@@ -9,6 +9,8 @@ import { navigateTo } from '@/navigation/navigateTo'
|
||||
import humanizeDuration from 'humanize-duration'
|
||||
import i18n from 'i18next'
|
||||
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||
import { useSettingsDialog } from '@/features/settings/hook/useSettingsDialog'
|
||||
import { SettingsDialogExtendedKey } from '@/features/settings/type'
|
||||
|
||||
const IDLE_DISCONNECT_TIMEOUT_MS = 120000 // 2 minutes
|
||||
const COUNTDOWN_ANNOUNCEMENT_SECONDS = [90, 60, 30]
|
||||
@@ -18,6 +20,7 @@ export const IsIdleDisconnectModal = () => {
|
||||
const connectionObserverSnap = useSnapshot(connectionObserverStore)
|
||||
const [timeRemaining, setTimeRemaining] = useState(IDLE_DISCONNECT_TIMEOUT_MS)
|
||||
const lastAnnouncementRef = useRef<number | null>(null)
|
||||
const { openSettingsDialog } = useSettingsDialog()
|
||||
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'isIdleDisconnectModal' })
|
||||
const announce = useScreenReaderAnnounce()
|
||||
@@ -117,7 +120,19 @@ export const IsIdleDisconnectModal = () => {
|
||||
}),
|
||||
})}
|
||||
</P>
|
||||
<P>{t('settings')}</P>
|
||||
<P>
|
||||
{t('settingsPrefix')}{' '}
|
||||
<A
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
connectionObserverStore.isIdleDisconnectModalOpen = false
|
||||
openSettingsDialog(SettingsDialogExtendedKey.GENERAL)
|
||||
}}
|
||||
>
|
||||
{t('settingsLink')}
|
||||
</A>
|
||||
{t('settingsSuffix')}
|
||||
</P>
|
||||
<HStack marginTop="2rem">
|
||||
<Button
|
||||
onPress={() => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ParticipantsList } from './controls/Participants/ParticipantsList'
|
||||
import { useSidePanel } from '../hooks/useSidePanel'
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode, useEffect, useRef } from 'react'
|
||||
import { Chat } from '../prefabs/Chat'
|
||||
import { Effects } from './effects/Effects'
|
||||
import { Admin } from './Admin'
|
||||
@@ -15,9 +15,11 @@ import { Tools } from './Tools'
|
||||
import { Info } from './Info'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
|
||||
const SIDE_PANEL_HEADING_ID = 'side-panel-heading'
|
||||
const SIDE_PANEL_CLOSE_ID = 'side-panel-close'
|
||||
|
||||
type StyledSidePanelProps = {
|
||||
title: string
|
||||
ariaLabel: string
|
||||
children: ReactNode
|
||||
onClose: () => void
|
||||
isClosed: boolean
|
||||
@@ -29,7 +31,6 @@ type StyledSidePanelProps = {
|
||||
|
||||
const StyledSidePanel = ({
|
||||
title,
|
||||
ariaLabel,
|
||||
children,
|
||||
onClose,
|
||||
isClosed,
|
||||
@@ -38,7 +39,14 @@ const StyledSidePanel = ({
|
||||
onBack,
|
||||
backButtonLabel,
|
||||
}: StyledSidePanelProps) => (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- role="dialog" makes this interactive
|
||||
<aside
|
||||
role="dialog"
|
||||
aria-labelledby={!isClosed ? SIDE_PANEL_HEADING_ID : undefined}
|
||||
aria-hidden={isClosed || undefined}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}}
|
||||
className={css({
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
@@ -63,57 +71,57 @@ const StyledSidePanel = ({
|
||||
style={{
|
||||
transform: isClosed ? 'translateX(calc(360px + 1.5rem))' : 'none',
|
||||
}}
|
||||
aria-hidden={isClosed}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<HStack alignItems="center">
|
||||
{isSubmenu && (
|
||||
<Button
|
||||
variant="secondaryText"
|
||||
size="sm"
|
||||
square
|
||||
className={css({ marginRight: '0.5rem', marginLeft: '1rem' })}
|
||||
aria-label={backButtonLabel}
|
||||
onPress={onBack}
|
||||
{isSubmenu && (
|
||||
<Button
|
||||
variant="secondaryText"
|
||||
size="sm"
|
||||
square
|
||||
className={css({ marginRight: '0.5rem', marginLeft: '1rem' })}
|
||||
aria-label={backButtonLabel}
|
||||
onPress={onBack}
|
||||
>
|
||||
<RiArrowLeftLine size={20} aria-hidden="true" />
|
||||
</Button>
|
||||
)}
|
||||
<Heading
|
||||
id={SIDE_PANEL_HEADING_ID}
|
||||
slot="title"
|
||||
level={1}
|
||||
className={text({ variant: 'h2' })}
|
||||
style={{
|
||||
paddingLeft: isSubmenu ? 0 : '1.5rem',
|
||||
paddingTop: '1rem',
|
||||
display: isClosed ? 'none' : 'flex',
|
||||
justifyContent: 'start',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<RiArrowLeftLine size={20} aria-hidden="true" />
|
||||
</Button>
|
||||
)}
|
||||
<Heading
|
||||
slot="title"
|
||||
level={1}
|
||||
className={text({ variant: 'h2' })}
|
||||
{title}
|
||||
</Heading>
|
||||
</HStack>
|
||||
<Div
|
||||
position="absolute"
|
||||
top="5"
|
||||
right="5"
|
||||
style={{
|
||||
paddingLeft: isSubmenu ? 0 : '1.5rem',
|
||||
paddingTop: '1rem',
|
||||
display: isClosed ? 'none' : 'flex',
|
||||
justifyContent: 'start',
|
||||
alignItems: 'center',
|
||||
display: isClosed ? 'none' : undefined,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Heading>
|
||||
</HStack>
|
||||
<Div
|
||||
position="absolute"
|
||||
top="5"
|
||||
right="5"
|
||||
style={{
|
||||
display: isClosed ? 'none' : undefined,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
invisible
|
||||
variant="tertiaryText"
|
||||
size="xs"
|
||||
onPress={onClose}
|
||||
aria-label={closeButtonTooltip}
|
||||
tooltip={closeButtonTooltip}
|
||||
>
|
||||
<RiCloseLine />
|
||||
</Button>
|
||||
</Div>
|
||||
{children}
|
||||
<Button
|
||||
id={SIDE_PANEL_CLOSE_ID}
|
||||
invisible
|
||||
variant="tertiaryText"
|
||||
size="xs"
|
||||
onPress={onClose}
|
||||
aria-label={closeButtonTooltip}
|
||||
tooltip={closeButtonTooltip}
|
||||
>
|
||||
<RiCloseLine />
|
||||
</Button>
|
||||
</Div>
|
||||
{children}
|
||||
</aside>
|
||||
)
|
||||
|
||||
@@ -135,6 +143,7 @@ const Panel = ({ isOpen, keepAlive = false, children }: PanelProps) => (
|
||||
{keepAlive || isOpen ? children : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const SidePanel = () => {
|
||||
const {
|
||||
activePanelId,
|
||||
@@ -150,14 +159,51 @@ export const SidePanel = () => {
|
||||
} = useSidePanel()
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
|
||||
|
||||
const triggerRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
// The aside stays mounted (CSS slide + keepAlive), so we manually handle
|
||||
// auto-focus on open and restore focus on close (via handleClose).
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSidePanelOpen) return
|
||||
const active = document.activeElement as HTMLElement
|
||||
// Menu items render as DIVs that unmount when the menu closes — resolve to the menu trigger
|
||||
triggerRef.current =
|
||||
active?.tagName === 'DIV'
|
||||
? (document.querySelector<HTMLElement>('#room-options-trigger') ??
|
||||
active)
|
||||
: active
|
||||
requestAnimationFrame(() => {
|
||||
const closeBtn = document.getElementById(SIDE_PANEL_CLOSE_ID)
|
||||
// Skip if a child panel already moved focus inside (e.g. Chat input)
|
||||
if (closeBtn?.closest('aside')?.contains(document.activeElement)) return
|
||||
closeBtn?.focus({ preventScroll: true })
|
||||
})
|
||||
}, [isSidePanelOpen])
|
||||
|
||||
const handleClose = () => {
|
||||
const trigger = triggerRef.current
|
||||
triggerRef.current = null
|
||||
layoutStore.activePanelId = null
|
||||
layoutStore.activeSubPanelId = null
|
||||
// Double RAF: first lets React re-render, second lets FocusScope release containment
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (trigger?.isConnected) {
|
||||
trigger.focus({ preventScroll: true })
|
||||
} else {
|
||||
document
|
||||
.querySelector<HTMLElement>('#room-options-trigger')
|
||||
?.focus({ preventScroll: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledSidePanel
|
||||
title={t(`heading.${activeSubPanelId || activePanelId}`)}
|
||||
ariaLabel={t('ariaLabel')}
|
||||
onClose={() => {
|
||||
layoutStore.activePanelId = null
|
||||
layoutStore.activeSubPanelId = null
|
||||
}}
|
||||
onClose={handleClose}
|
||||
closeButtonTooltip={t('closeButton', {
|
||||
content: t(`content.${activeSubPanelId || activePanelId}`),
|
||||
})}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Button as RACButton } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReactNode } from 'react'
|
||||
import { SubPanelId, useSidePanel } from '../hooks/useSidePanel'
|
||||
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
|
||||
import {
|
||||
useIsRecordingModeEnabled,
|
||||
RecordingMode,
|
||||
@@ -95,26 +94,10 @@ const ToolButton = ({
|
||||
|
||||
export const Tools = () => {
|
||||
const { data } = useConfig()
|
||||
const { openTranscript, openScreenRecording, activeSubPanelId, isToolsOpen } =
|
||||
const { openTranscript, openScreenRecording, activeSubPanelId } =
|
||||
useSidePanel()
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
|
||||
|
||||
// Restore focus to the element that opened the Tools panel
|
||||
// following the same pattern as Chat.
|
||||
useRestoreFocus(isToolsOpen, {
|
||||
// If the active element is a MenuItem (DIV) that will be unmounted when the menu closes,
|
||||
// find the "more options" button ("Plus d'options") that opened the menu
|
||||
resolveTrigger: (activeEl) => {
|
||||
if (activeEl?.tagName === 'DIV') {
|
||||
return document.querySelector<HTMLElement>('#room-options-trigger')
|
||||
}
|
||||
// For direct button clicks (e.g. "Plus d'outils"), use the active element as is
|
||||
return activeEl
|
||||
},
|
||||
restoreFocusRaf: true,
|
||||
preventScroll: true,
|
||||
})
|
||||
|
||||
const isTranscriptEnabled = useIsRecordingModeEnabled(
|
||||
RecordingMode.Transcript
|
||||
)
|
||||
|
||||
+1
-3
@@ -11,9 +11,7 @@ import {
|
||||
ProcessorType,
|
||||
} from '.'
|
||||
|
||||
export class UnifiedBackgroundTrackProcessor
|
||||
implements BackgroundProcessorInterface
|
||||
{
|
||||
export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface {
|
||||
processor: ProcessorWrapper<BackgroundOptions>
|
||||
opts: BackgroundOptions
|
||||
processorType: ProcessorType
|
||||
|
||||
@@ -13,8 +13,7 @@ export interface ProcessorSerialized {
|
||||
options: BackgroundOptions
|
||||
}
|
||||
|
||||
export interface BackgroundProcessorInterface
|
||||
extends TrackProcessor<Track.Kind> {
|
||||
export interface BackgroundProcessorInterface extends TrackProcessor<Track.Kind> {
|
||||
update(opts: BackgroundOptions): Promise<void>
|
||||
options: BackgroundOptions
|
||||
clone(): BackgroundProcessorInterface
|
||||
|
||||
@@ -80,10 +80,12 @@ export const ChatInput = ({
|
||||
<TextArea
|
||||
ref={inputRef}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.key !== 'Escape') e.stopPropagation()
|
||||
submitOnEnter(e)
|
||||
}}
|
||||
onKeyUp={(e) => e.stopPropagation()}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key !== 'Escape') e.stopPropagation()
|
||||
}}
|
||||
placeholder={t('textArea.placeholder')}
|
||||
value={text}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ToggleButton } from '@/primitives'
|
||||
import { chatStore } from '@/stores/chat'
|
||||
import { useSidePanel } from '../../hooks/useSidePanel'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
|
||||
export const ChatToggle = ({
|
||||
onPress,
|
||||
@@ -18,6 +19,11 @@ export const ChatToggle = ({
|
||||
const { isChatOpen, toggleChat } = useSidePanel()
|
||||
const tooltipLabel = isChatOpen ? 'open' : 'closed'
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'toggle-chat',
|
||||
handler: toggleChat,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
|
||||
import { useDeviceShortcut } from '../../../hooks/useDeviceShortcut'
|
||||
import { ToggleSource, CaptureOptionsBySource } from '@livekit/components-core'
|
||||
import { getShortcutDescriptorById } from '@/features/shortcuts/catalog'
|
||||
|
||||
type ToggleDeviceStyleProps = {
|
||||
variant?: NonNullable<ButtonRecipeProps>['variant']
|
||||
@@ -88,12 +89,14 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
const deviceShortcut = useDeviceShortcut(kind)
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
shortcut: deviceShortcut,
|
||||
id: deviceShortcut?.id,
|
||||
handler: async () => await toggle(),
|
||||
isDisabled: cannotUseDevice,
|
||||
})
|
||||
|
||||
const pushToTalkShortcut = getShortcutDescriptorById('push-to-talk')
|
||||
useLongPress({
|
||||
keyCode: kind === 'audioinput' ? 'KeyV' : undefined,
|
||||
keyCode: kind === 'audioinput' ? pushToTalkShortcut?.code : undefined,
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
isDisabled: cannotUseDevice,
|
||||
@@ -103,7 +106,9 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
const label = t(enabled ? 'disable' : 'enable', {
|
||||
keyPrefix: `selectDevice.${kind}`,
|
||||
})
|
||||
return deviceShortcut ? appendShortcutLabel(label, deviceShortcut) : label
|
||||
return deviceShortcut?.shortcut
|
||||
? appendShortcutLabel(label, deviceShortcut.shortcut)
|
||||
: label
|
||||
}, [enabled, kind, deviceShortcut, t])
|
||||
|
||||
const Icon =
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
closeLowerHandToasts,
|
||||
showLowerHandToast,
|
||||
} from '@/features/notifications/utils'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
|
||||
const SPEAKING_DETECTION_DELAY = 3000
|
||||
|
||||
@@ -33,6 +34,16 @@ export const HandToggle = () => {
|
||||
closeLowerHandToasts()
|
||||
}, [isHandRaised])
|
||||
|
||||
const handleToggle = () => {
|
||||
toggleRaisedHand()
|
||||
resetToastState()
|
||||
}
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'raise-hand',
|
||||
handler: handleToggle,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const shouldShowToast = isSpeaking && isHandRaised && !hasShownToast
|
||||
|
||||
@@ -68,10 +79,7 @@ export const HandToggle = () => {
|
||||
aria-label={t(tooltipLabel)}
|
||||
tooltip={t(tooltipLabel)}
|
||||
isSelected={isHandRaised}
|
||||
onPress={() => {
|
||||
toggleRaisedHand()
|
||||
resetToastState()
|
||||
}}
|
||||
onPress={handleToggle}
|
||||
data-attr={`controls-hand-${tooltipLabel}`}
|
||||
>
|
||||
<RiHand />
|
||||
|
||||
+6
@@ -6,6 +6,7 @@ import { css } from '@/styled-system/css'
|
||||
import { useParticipants } from '@livekit/components-react'
|
||||
import { useSidePanel } from '../../../hooks/useSidePanel'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
|
||||
export const ParticipantsToggle = ({
|
||||
onPress,
|
||||
@@ -27,6 +28,11 @@ export const ParticipantsToggle = ({
|
||||
|
||||
const tooltipLabel = isParticipantsOpen ? 'open' : 'closed'
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'toggle-participants',
|
||||
handler: toggleParticipants,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ReactionPortals,
|
||||
} from '@/features/rooms/livekit/components/ReactionPortal'
|
||||
import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
import { Toolbar as RACToolbar } from 'react-aria-components'
|
||||
import { Participant } from 'livekit-client'
|
||||
import useRateLimiter from '@/hooks/useRateLimiter'
|
||||
@@ -41,6 +42,11 @@ export const ReactionsToggle = () => {
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'reaction',
|
||||
handler: () => setIsVisible((prev) => !prev),
|
||||
})
|
||||
|
||||
const sendReaction = async (emoji: string) => {
|
||||
const encoder = new TextEncoder()
|
||||
const payload: NotificationPayload = {
|
||||
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
import React from 'react'
|
||||
|
||||
/** @public */
|
||||
export interface AllowMediaPlaybackProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
export interface AllowMediaPlaybackProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
label?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ const ASPECT_RATIO = 16 / 10
|
||||
const ASPECT_RATIO_INVERT = (1 - ASPECT_RATIO) * -1
|
||||
|
||||
/** @public */
|
||||
export interface CarouselLayoutProps
|
||||
extends React.HTMLAttributes<HTMLMediaElement> {
|
||||
export interface CarouselLayoutProps extends React.HTMLAttributes<HTMLMediaElement> {
|
||||
tracks: TrackReferenceOrPlaceholder[]
|
||||
children: React.ReactNode
|
||||
/** Place the tiles vertically or horizontally next to each other.
|
||||
|
||||
@@ -13,7 +13,8 @@ import { PaginationControl } from '../controls/PaginationControl'
|
||||
|
||||
/** @public */
|
||||
export interface GridLayoutProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
extends
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
Pick<UseParticipantsOptions, 'updateOnlyOn'> {
|
||||
children: React.ReactNode
|
||||
tracks: TrackReferenceOrPlaceholder[]
|
||||
|
||||
@@ -12,6 +12,6 @@ export function useCanPublishTrack(trackSource: TrackSource): boolean {
|
||||
|
||||
return Boolean(
|
||||
permissions?.canPublish &&
|
||||
permissions?.canPublishSources?.includes(trackSource)
|
||||
permissions?.canPublishSources?.includes(trackSource)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Shortcut } from '@/features/shortcuts/types'
|
||||
import {
|
||||
getShortcutDescriptorById,
|
||||
ShortcutDescriptor,
|
||||
} from '@/features/shortcuts/catalog'
|
||||
|
||||
export const useDeviceShortcut = (kind: MediaDeviceKind) => {
|
||||
return useMemo<Shortcut | undefined>(() => {
|
||||
return useMemo<ShortcutDescriptor | undefined>(() => {
|
||||
switch (kind) {
|
||||
case 'audioinput':
|
||||
return {
|
||||
key: 'd',
|
||||
ctrlKey: true,
|
||||
}
|
||||
return getShortcutDescriptorById('toggle-microphone')
|
||||
case 'videoinput':
|
||||
return {
|
||||
key: 'e',
|
||||
ctrlKey: true,
|
||||
}
|
||||
return getShortcutDescriptorById('toggle-camera')
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -15,11 +15,9 @@ import { ChatEntry } from '../components/chat/Entry'
|
||||
import { useSidePanel } from '../hooks/useSidePanel'
|
||||
import { LocalParticipant, RemoteParticipant, RoomEvent } from 'livekit-client'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
|
||||
|
||||
export interface ChatProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
ChatOptions {}
|
||||
extends React.HTMLAttributes<HTMLDivElement>, ChatOptions {}
|
||||
|
||||
/**
|
||||
* The Chat component adds a basis chat functionality to the LiveKit room. The messages are distributed to all participants
|
||||
@@ -37,18 +35,12 @@ export function Chat({ ...props }: ChatProps) {
|
||||
const { isChatOpen } = useSidePanel()
|
||||
const chatSnap = useSnapshot(chatStore)
|
||||
|
||||
// Keep track of the element that opened the chat so we can restore focus
|
||||
// when the chat panel is closed.
|
||||
useRestoreFocus(isChatOpen, {
|
||||
// Avoid layout "jump" during the side panel slide-in animation.
|
||||
// Focusing can trigger scroll into view; preventScroll keeps the animation smooth.
|
||||
onOpened: () => {
|
||||
requestAnimationFrame(() => {
|
||||
inputRef.current?.focus({ preventScroll: true })
|
||||
})
|
||||
},
|
||||
preventScroll: true,
|
||||
})
|
||||
React.useEffect(() => {
|
||||
if (!isChatOpen) return
|
||||
requestAnimationFrame(() => {
|
||||
inputRef.current?.focus({ preventScroll: true })
|
||||
})
|
||||
}, [isChatOpen])
|
||||
|
||||
// Use useParticipants hook to trigger a re-render when the participant list changes.
|
||||
const participants = useParticipants()
|
||||
|
||||
@@ -22,7 +22,7 @@ export function DesktopControlBar({
|
||||
const desktopControlBarEl = useRef<HTMLDivElement>(null)
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
shortcut: { key: 'F2' },
|
||||
id: 'focus-toolbar',
|
||||
handler: () => {
|
||||
const root = desktopControlBarEl.current
|
||||
if (!root) return
|
||||
|
||||
@@ -66,8 +66,7 @@ const LayoutWrapper = styled(
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface VideoConferenceProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** @alpha */
|
||||
SettingsComponent?: React.ComponentType
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ import NoiseSuppressorWorklet from '@timephy/rnnoise-wasm/NoiseSuppressorWorklet
|
||||
// and suspend/resume it as needed to manage audio state
|
||||
let audioContext: AudioContext
|
||||
|
||||
export interface AudioProcessorInterface
|
||||
extends TrackProcessor<Track.Kind.Audio> {
|
||||
export interface AudioProcessorInterface extends TrackProcessor<Track.Kind.Audio> {
|
||||
name: string
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Shortcut } from './types'
|
||||
|
||||
// Central list of current keyboard shortcuts.
|
||||
// Keep a single source of truth for display and, later, customization
|
||||
export type ShortcutCategory = 'navigation' | 'media' | 'interaction'
|
||||
|
||||
export type ShortcutId =
|
||||
| 'focus-toolbar'
|
||||
| 'toggle-microphone'
|
||||
| 'toggle-camera'
|
||||
| 'push-to-talk'
|
||||
| 'toggle-chat'
|
||||
| 'toggle-participants'
|
||||
| 'raise-hand'
|
||||
| 'recording'
|
||||
| 'reaction'
|
||||
| 'fullscreen'
|
||||
|
||||
export const getShortcutDescriptorById = (id: ShortcutId) =>
|
||||
shortcutCatalog.find((item) => item.id === id)
|
||||
|
||||
export type ShortcutDescriptor = {
|
||||
id: ShortcutId
|
||||
category: ShortcutCategory
|
||||
shortcut?: Shortcut
|
||||
kind?: 'press' | 'longPress'
|
||||
code?: string // used when kind === 'longPress' (KeyboardEvent.code)
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const shortcutCatalog: ShortcutDescriptor[] = [
|
||||
{
|
||||
id: 'focus-toolbar',
|
||||
category: 'navigation',
|
||||
shortcut: { key: 'F2' },
|
||||
},
|
||||
{
|
||||
id: 'toggle-microphone',
|
||||
category: 'media',
|
||||
shortcut: { key: 'd', ctrlKey: true },
|
||||
},
|
||||
{
|
||||
id: 'toggle-camera',
|
||||
category: 'media',
|
||||
shortcut: { key: 'e', ctrlKey: true },
|
||||
},
|
||||
{
|
||||
id: 'push-to-talk',
|
||||
category: 'media',
|
||||
kind: 'longPress',
|
||||
code: 'KeyV',
|
||||
},
|
||||
{
|
||||
id: 'reaction',
|
||||
category: 'interaction',
|
||||
shortcut: { key: 'E', ctrlKey: true, shiftKey: true },
|
||||
},
|
||||
{
|
||||
id: 'fullscreen',
|
||||
category: 'interaction',
|
||||
shortcut: { key: 'F', ctrlKey: true, shiftKey: true },
|
||||
},
|
||||
{
|
||||
id: 'recording',
|
||||
category: 'interaction',
|
||||
shortcut: { key: 'L', ctrlKey: true, shiftKey: true },
|
||||
},
|
||||
{
|
||||
id: 'raise-hand',
|
||||
category: 'interaction',
|
||||
shortcut: { key: 'H', ctrlKey: true, shiftKey: true },
|
||||
},
|
||||
{
|
||||
id: 'toggle-chat',
|
||||
category: 'interaction',
|
||||
shortcut: { key: 'M', ctrlKey: true, shiftKey: true },
|
||||
},
|
||||
{
|
||||
id: 'toggle-participants',
|
||||
category: 'interaction',
|
||||
shortcut: { key: 'P', ctrlKey: true, shiftKey: true },
|
||||
},
|
||||
]
|
||||
@@ -1,4 +1,6 @@
|
||||
export type Shortcut = {
|
||||
key: string
|
||||
ctrlKey?: boolean
|
||||
shiftKey?: boolean
|
||||
altKey?: boolean
|
||||
}
|
||||
|
||||
@@ -11,11 +11,13 @@ export const useKeyboardShortcuts = () => {
|
||||
// This approach handles basic shortcuts but isn't comprehensive.
|
||||
// Issues might occur. First draft.
|
||||
const onKeyDown = async (e: KeyboardEvent) => {
|
||||
const { key, metaKey, ctrlKey } = e
|
||||
const { key, metaKey, ctrlKey, shiftKey, altKey } = e
|
||||
if (!key) return
|
||||
const shortcutKey = formatShortcutKey({
|
||||
key,
|
||||
ctrlKey: ctrlKey || (isMacintosh() && metaKey),
|
||||
shiftKey,
|
||||
altKey,
|
||||
})
|
||||
const shortcut = shortcutsSnap.shortcuts.get(shortcutKey)
|
||||
if (!shortcut) return
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { useEffect } from 'react'
|
||||
import { keyboardShortcutsStore } from '@/stores/keyboardShortcuts'
|
||||
import { formatShortcutKey } from '@/features/shortcuts/utils'
|
||||
import { Shortcut } from '@/features/shortcuts/types'
|
||||
import { ShortcutId, getShortcutDescriptorById } from './catalog'
|
||||
|
||||
export type useRegisterKeyboardShortcutProps = {
|
||||
shortcut?: Shortcut
|
||||
id?: ShortcutId
|
||||
handler: () => Promise<void | boolean | undefined> | void
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
export const useRegisterKeyboardShortcut = ({
|
||||
shortcut,
|
||||
id,
|
||||
handler,
|
||||
isDisabled = false,
|
||||
}: useRegisterKeyboardShortcutProps) => {
|
||||
useEffect(() => {
|
||||
if (!shortcut) return
|
||||
const formattedKey = formatShortcutKey(shortcut)
|
||||
if (!id) return
|
||||
const descriptor = getShortcutDescriptorById(id)
|
||||
if (!descriptor?.shortcut) return
|
||||
const formattedKey = formatShortcutKey(descriptor.shortcut)
|
||||
if (isDisabled) {
|
||||
keyboardShortcutsStore.shortcuts.delete(formattedKey)
|
||||
} else {
|
||||
keyboardShortcutsStore.shortcuts.set(formattedKey, handler)
|
||||
}
|
||||
}, [handler, shortcut, isDisabled])
|
||||
}, [handler, id, isDisabled])
|
||||
}
|
||||
|
||||
@@ -4,15 +4,27 @@ import { Shortcut } from '@/features/shortcuts/types'
|
||||
export const CTRL = 'ctrl'
|
||||
|
||||
export const formatShortcutKey = (shortcut: Shortcut) => {
|
||||
if (shortcut.ctrlKey) return `${CTRL}+${shortcut.key.toUpperCase()}`
|
||||
return shortcut.key.toUpperCase()
|
||||
const parts = []
|
||||
if (shortcut.ctrlKey) parts.push(CTRL)
|
||||
if (shortcut.altKey) parts.push('alt')
|
||||
if (shortcut.shiftKey) parts.push('shift')
|
||||
parts.push(shortcut.key.toUpperCase())
|
||||
return parts.join('+')
|
||||
}
|
||||
|
||||
export const appendShortcutLabel = (label: string, shortcut: Shortcut) => {
|
||||
if (!shortcut.key) return
|
||||
let formattedKeyLabel = shortcut.key.toLowerCase()
|
||||
const parts: string[] = []
|
||||
if (shortcut.ctrlKey) {
|
||||
formattedKeyLabel = `${isMacintosh() ? '⌘' : 'Ctrl'}+${formattedKeyLabel}`
|
||||
parts.push(isMacintosh() ? '⌘' : 'Ctrl')
|
||||
}
|
||||
if (shortcut.altKey) {
|
||||
parts.push(isMacintosh() ? '⌥' : 'Alt')
|
||||
}
|
||||
if (shortcut.shiftKey) {
|
||||
parts.push('Shift')
|
||||
}
|
||||
parts.push(shortcut.key.toLowerCase())
|
||||
const formattedKeyLabel = parts.join('+')
|
||||
return `${label} (${formattedKeyLabel})`
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ export interface TranscriptionSegment {
|
||||
lastReceivedTime: number
|
||||
}
|
||||
|
||||
export interface TranscriptionSegmentWithParticipant
|
||||
extends TranscriptionSegment {
|
||||
export interface TranscriptionSegmentWithParticipant extends TranscriptionSegment {
|
||||
participant: Participant
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,9 @@
|
||||
"isIdleDisconnectModal": {
|
||||
"title": "Bist du noch da?",
|
||||
"body": "Du bist der einzige Teilnehmer. Dieses Gespräch endet in {{duration}}. Möchtest du das Gespräch fortsetzen?",
|
||||
"settings": "Um diese Nachricht nicht mehr zu sehen, gehe zu Einstellungen > Allgemein.",
|
||||
"settingsPrefix": "Um diese Nachricht nicht mehr zu sehen, gehe zu",
|
||||
"settingsLink": "Einstellungen > Allgemein",
|
||||
"settingsSuffix": ".",
|
||||
"stayButton": "Gespräch fortsetzen",
|
||||
"leaveButton": "Jetzt verlassen",
|
||||
"countdownAnnouncement": "Das Gespräch endet in {{duration}}."
|
||||
|
||||
@@ -108,12 +108,18 @@
|
||||
"label": "Sprache"
|
||||
},
|
||||
"settingsButtonLabel": "Einstellungen",
|
||||
"accessibility": {
|
||||
"announceReactions": {
|
||||
"label": "Reaktionen vorlesen"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"account": "Profil",
|
||||
"audio": "Audio",
|
||||
"video": "Video",
|
||||
"general": "Allgemein",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"accessibility": "Barrierefreiheit",
|
||||
"transcription": "Transkription"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,9 @@
|
||||
"isIdleDisconnectModal": {
|
||||
"title": "Are you still there?",
|
||||
"body": "You are the only participant. This call will end in {{duration}}. Would you like to continue the call?",
|
||||
"settings": "To stop seeing this message, go to Settings > General.",
|
||||
"settingsPrefix": "To stop seeing this message, go to",
|
||||
"settingsLink": "Settings > General",
|
||||
"settingsSuffix": ".",
|
||||
"stayButton": "Continue the call",
|
||||
"leaveButton": "Leave now",
|
||||
"countdownAnnouncement": "Call ends in {{duration}}."
|
||||
|
||||
@@ -154,7 +154,9 @@
|
||||
"isIdleDisconnectModal": {
|
||||
"title": "Êtes-vous toujours là ?",
|
||||
"body": "Vous êtes le seul participant. Cet appel va donc se terminer dans {{duration}}. Voulez-vous poursuivre l'appel ?",
|
||||
"settings": "Pour ne plus voir ce message, accèder à Paramètres > Général.",
|
||||
"settingsPrefix": "Pour ne plus voir ce message, allez dans",
|
||||
"settingsLink": "Paramètres > Général",
|
||||
"settingsSuffix": ".",
|
||||
"stayButton": "Poursuivre l'appel",
|
||||
"leaveButton": "Quitter maintenant",
|
||||
"countdownAnnouncement": "L'appel se termine dans {{duration}}."
|
||||
|
||||
@@ -154,7 +154,9 @@
|
||||
"isIdleDisconnectModal": {
|
||||
"title": "Ben je er nog?",
|
||||
"body": "Je bent de enige deelnemer. Dit gesprek wordt over {{duration}} beëindigd. Wil je het gesprek voortzetten?",
|
||||
"settings": "Om dit bericht niet meer te zien, ga naar Instellingen > Algemeen.",
|
||||
"settingsPrefix": "Om dit bericht niet meer te zien, ga naar",
|
||||
"settingsLink": "Instellingen > Algemeen",
|
||||
"settingsSuffix": ".",
|
||||
"stayButton": "Gesprek voortzetten",
|
||||
"leaveButton": "Nu verlaten",
|
||||
"countdownAnnouncement": "Het gesprek eindigt over {{duration}}."
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { ProgressBar } from 'react-aria-components'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { RiHourglassFill } from '@remixicon/react'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
|
||||
const rotatingArcClassName = css({
|
||||
animation: 'rotate 1s ease-in-out infinite',
|
||||
transformOrigin: 'center',
|
||||
transition: 'transform 16ms linear',
|
||||
})
|
||||
|
||||
export const Spinner = ({
|
||||
size = 56,
|
||||
@@ -13,44 +20,77 @@ export const Spinner = ({
|
||||
const r = 14 - strokeWidth
|
||||
const c = 2 * r * Math.PI
|
||||
return (
|
||||
<ProgressBar aria-label="Loading…" value={30}>
|
||||
<ProgressBar aria-label="Loading..." value={30}>
|
||||
{({ percentage }) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth}
|
||||
<div
|
||||
className={css({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
})}
|
||||
>
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={r}
|
||||
strokeDasharray={0}
|
||||
strokeDashoffset={0}
|
||||
strokeLinecap="round"
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth}
|
||||
className={css({
|
||||
stroke: variant == 'light' ? 'primary.100' : 'transparent',
|
||||
'@media (prefers-reduced-motion: reduce)': {
|
||||
display: 'none',
|
||||
},
|
||||
})}
|
||||
style={{}}
|
||||
/>
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={r}
|
||||
strokeDasharray={`${c} ${c}`}
|
||||
strokeDashoffset={percentage && c - (percentage / 100) * c}
|
||||
strokeLinecap="round"
|
||||
>
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={r}
|
||||
strokeDasharray={0}
|
||||
strokeDashoffset={0}
|
||||
strokeLinecap="round"
|
||||
className={css({
|
||||
stroke: variant == 'light' ? 'primary.100' : 'transparent',
|
||||
})}
|
||||
/>
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={r}
|
||||
strokeDasharray={`${c} ${c}`}
|
||||
strokeDashoffset={
|
||||
typeof percentage === 'number'
|
||||
? c - (percentage / 100) * c
|
||||
: undefined
|
||||
}
|
||||
strokeLinecap="round"
|
||||
className={cx(
|
||||
rotatingArcClassName,
|
||||
css({
|
||||
stroke: variant == 'light' ? 'primary.800' : 'white',
|
||||
})
|
||||
)}
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={css({
|
||||
stroke: variant == 'light' ? 'primary.800' : 'white',
|
||||
display: 'none',
|
||||
color: 'black',
|
||||
'@media (prefers-reduced-motion: reduce)': {
|
||||
display: 'inline-flex',
|
||||
},
|
||||
})}
|
||||
style={{
|
||||
animation: `rotate 1s ease-in-out infinite`,
|
||||
transformOrigin: 'center',
|
||||
transition: 'transform 16ms linear',
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
>
|
||||
<RiHourglassFill
|
||||
size={Math.max(16, Math.round(size * 0.4))}
|
||||
style={{
|
||||
display: 'block',
|
||||
transform: 'translateY(1px)',
|
||||
color: variant == 'light' ? 'primary.800' : 'white',
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</ProgressBar>
|
||||
)
|
||||
|
||||
@@ -75,6 +75,10 @@ backend:
|
||||
ROOM_TELEPHONY_PHONE_NUMBER: '+33901020304'
|
||||
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
|
||||
ROOM_SUBTITLE_ENABLED: True
|
||||
EXTERNAL_API_ENABLED: True
|
||||
APPLICATION_JWT_AUDIENCE: https://meet.127.0.0.1.nip.io/external-api/v1.0/
|
||||
APPLICATION_JWT_SECRET_KEY: devKeyApplication
|
||||
APPLICATION_BASE_URL: https://meet.127.0.0.1.nip.io
|
||||
|
||||
|
||||
migrate:
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.0",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "1.5.0"
|
||||
version = "1.7.0"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
Reference in New Issue
Block a user