Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca9a9288ab |
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Crowdin files
|
||||
uses: crowdin/github-action@v2
|
||||
|
||||
@@ -23,13 +23,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
@@ -55,7 +49,6 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
target: backend-production
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -66,13 +59,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
@@ -99,7 +86,6 @@ jobs:
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -110,13 +96,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
@@ -143,7 +123,6 @@ jobs:
|
||||
context: .
|
||||
file: ./docker/dinum-frontend/Dockerfile
|
||||
target: frontend-production
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -154,13 +133,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
@@ -189,7 +162,6 @@ jobs:
|
||||
context: ./src/summary
|
||||
file: ./src/summary/Dockerfile
|
||||
target: production
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -200,13 +172,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
@@ -235,7 +201,6 @@ jobs:
|
||||
context: ./src/agents
|
||||
file: ./src/agents/Dockerfile
|
||||
target: production
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
+20
-43
@@ -7,18 +7,14 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint-git:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
@@ -43,11 +39,9 @@ jobs:
|
||||
if: |
|
||||
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
|
||||
github.event_name == 'pull_request'
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
@@ -55,11 +49,9 @@ jobs:
|
||||
|
||||
lint-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
@@ -70,22 +62,20 @@ jobs:
|
||||
|
||||
build-mails:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/mail
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
@@ -105,23 +95,21 @@ jobs:
|
||||
|
||||
- name: Cache mail templates
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
lint-back:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
@@ -136,16 +124,14 @@ jobs:
|
||||
|
||||
lint-agents:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/agents
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
@@ -158,16 +144,14 @@ jobs:
|
||||
|
||||
lint-summary:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/summary
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
@@ -181,8 +165,7 @@ jobs:
|
||||
test-back:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-mails
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/backend
|
||||
@@ -233,7 +216,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create writable /data
|
||||
run: |
|
||||
@@ -241,7 +224,7 @@ jobs:
|
||||
sudo mkdir -p /data/static
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
@@ -275,7 +258,7 @@ jobs:
|
||||
mc mb meet/meet-media-storage"
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
@@ -296,11 +279,9 @@ jobs:
|
||||
|
||||
lint-front:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/frontend/ && npm ci
|
||||
@@ -313,14 +294,12 @@ jobs:
|
||||
|
||||
lint-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -333,15 +312,13 @@ jobs:
|
||||
|
||||
build-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
needs: lint-sdk
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -8,50 +8,9 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- 👷(docker) add arm64 platform support for image builds
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(frontend) replace custom reactions toolbar with react aria popover #985
|
||||
- 🔒️(frontend) uninstall curl from the frontend production image #987
|
||||
- 💄(frontend) add focus ring to reaction emoji buttons
|
||||
- ✨(frontend) introduce a shortcut settings tab #975
|
||||
- 🚚(frontend) rename "wellknown" directory to "well-known" #1009
|
||||
- 🌐(frontend) localize SR modifier labels #1010
|
||||
- ⬆️(backend) update python dependencies #1011
|
||||
- ♿️(a11y) fix focus ring on tab container components
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🩹(frontend) fix German language preference update #1021
|
||||
|
||||
|
||||
## [1.8.0] - 2026-02-20
|
||||
|
||||
### Changed
|
||||
|
||||
- 🔒️(agents) uninstall pip from the agents image
|
||||
- 🔒️(summary) switch to Alpine base image
|
||||
- 🔒️(backend) uninstall pip in the production image
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔒️(agents) upgrade OpenSSL to address CVE-2025-15467
|
||||
- 📌(agents) pin protobuf to 6.33.5 to fix CVE-2026-0994
|
||||
|
||||
## [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
|
||||
- ♻️(backend) refactor external API token-related items #1006
|
||||
|
||||
## [1.6.0] - 2026-02-10
|
||||
|
||||
|
||||
@@ -127,9 +127,6 @@ ARG MEET_STATIC_ROOT=/data/static
|
||||
RUN mkdir -p /usr/local/etc/gunicorn
|
||||
COPY docker/files/usr/local/etc/gunicorn/meet.py /usr/local/etc/gunicorn/meet.py
|
||||
|
||||
# Remove pip to reduce attack surface in production
|
||||
RUN pip uninstall -y pip
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
@@ -23,10 +23,9 @@
|
||||
# ==============================================================================
|
||||
# VARIABLES
|
||||
|
||||
ESC := $(shell printf '\033')
|
||||
BOLD := $(ESC)[1m
|
||||
RESET := $(ESC)[0m
|
||||
GREEN := $(ESC)[1;32m
|
||||
BOLD := \033[1m
|
||||
RESET := \033[0m
|
||||
GREEN := \033[1;32m
|
||||
|
||||
|
||||
# -- Database
|
||||
@@ -86,8 +85,7 @@ bootstrap: \
|
||||
demo \
|
||||
back-i18n-compile \
|
||||
mails-install \
|
||||
mails-build \
|
||||
run
|
||||
mails-build
|
||||
.PHONY: bootstrap
|
||||
|
||||
# -- Docker/compose
|
||||
|
||||
+1
-5
@@ -18,7 +18,6 @@ docker_build(
|
||||
'localhost:5001/meet-backend:latest',
|
||||
context='..',
|
||||
dockerfile='../Dockerfile',
|
||||
build_args={'DOCKER_USER': '1001:127'},
|
||||
only=['./src/backend', './src/mail', './docker'],
|
||||
target = 'backend-production',
|
||||
live_update=[
|
||||
@@ -34,7 +33,6 @@ clean_old_images('localhost:5001/meet-backend')
|
||||
docker_build(
|
||||
'localhost:5001/meet-frontend-dinum:latest',
|
||||
context='..',
|
||||
build_args={'DOCKER_USER': '1001:127'},
|
||||
dockerfile='../docker/dinum-frontend/Dockerfile',
|
||||
only=['./src/frontend', './docker', './.dockerignore'],
|
||||
target = 'frontend-production',
|
||||
@@ -59,7 +57,6 @@ clean_old_images('localhost:5001/meet-frontend-generic')
|
||||
docker_build(
|
||||
'localhost:5001/meet-summary:latest',
|
||||
context='../src/summary',
|
||||
build_args={'DOCKER_USER': '1001:127'},
|
||||
dockerfile='../src/summary/Dockerfile',
|
||||
only=['.'],
|
||||
target = 'production',
|
||||
@@ -72,9 +69,8 @@ clean_old_images('localhost:5001/meet-summary')
|
||||
docker_build(
|
||||
'localhost:5001/meet-agents:latest',
|
||||
context='../src/agents',
|
||||
build_args={'DOCKER_USER': '1001:127'},
|
||||
dockerfile='../src/agents/Dockerfile',
|
||||
only=['.'],
|
||||
only=['.'],
|
||||
target = 'production',
|
||||
live_update=[
|
||||
sync('../src/agents', '/app'),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
upstream meet_backend {
|
||||
server ${BACKEND_INTERNAL_HOST}:8000 fail_timeout=0;
|
||||
server ${BACKEND_HOST}:8000 fail_timeout=0;
|
||||
}
|
||||
|
||||
upstream meet_frontend {
|
||||
server ${FRONTEND_INTERNAL_HOST}:8080 fail_timeout=0;
|
||||
server ${FRONTEND_HOST}:8080 fail_timeout=0;
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM livekit/livekit-server:v1.9.4
|
||||
FROM livekit/livekit-server:v1.9.0
|
||||
|
||||
# We inject the nip.io certificate manually because the livekit chart doesn't support volume mounting
|
||||
COPY rootCA.pem /etc/ssl/certs/
|
||||
|
||||
@@ -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,7 +45,6 @@ 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:
|
||||
|
||||
@@ -32,7 +32,7 @@ Set the following environment variables in your Scalingo app:
|
||||
### Buildpack Configuration
|
||||
|
||||
```bash
|
||||
scalingo env-set BUILDPACK_URL="https://github.com/suitenumerique/buildpack#main"
|
||||
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/"
|
||||
|
||||
@@ -3,21 +3,6 @@
|
||||
"dependencyDashboard": true,
|
||||
"labels": ["dependencies", "noChangeLog"],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "js dependencies",
|
||||
"matchManagers": ["npm"],
|
||||
"schedule": ["on the first day of the month"],
|
||||
"matchPackagePatterns": ["*"],
|
||||
"minimumReleaseAge": "7 days",
|
||||
"internalChecksFilter": "strict"
|
||||
},
|
||||
{
|
||||
"groupName": "python dependencies",
|
||||
"matchManagers": ["setup-cfg", "pep621"],
|
||||
"schedule": ["on the first day of the month"],
|
||||
"matchPackagePatterns": ["*"],
|
||||
"minimumReleaseAge": "7 days"
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"groupName": "ignored python dependencies",
|
||||
@@ -30,12 +15,6 @@
|
||||
"matchPackageNames": ["pylint"],
|
||||
"allowedVersions": "<4.0.0"
|
||||
},
|
||||
{
|
||||
"groupName": "allowed django versions",
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["django"],
|
||||
"allowedVersions": "<6.0.0"
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"groupName": "ignored js dependencies",
|
||||
|
||||
@@ -4,8 +4,6 @@ FROM python:3.13-slim AS base
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libglib2.0-0 \
|
||||
libgobject-2.0-0 \
|
||||
"openssl=3.5.4-1~deb13u2" \
|
||||
"libssl3t64=3.5.4-1~deb13u2" \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM base AS builder
|
||||
@@ -21,9 +19,6 @@ FROM base AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Remove pip to reduce attack surface in production
|
||||
RUN pip uninstall -y pip
|
||||
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.8.0"
|
||||
version = "1.6.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.3.10",
|
||||
"livekit-plugins-deepgram==1.3.10",
|
||||
"livekit-plugins-silero==1.3.10",
|
||||
"livekit-plugins-kyutai-lasuite==0.0.6",
|
||||
"python-dotenv==1.2.1",
|
||||
"protobuf==6.33.5"
|
||||
"python-dotenv==1.2.1"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -1,51 +1,27 @@
|
||||
"""Authentication Backends for external application to the Meet core app."""
|
||||
|
||||
# pylint: disable=R0913,R0917
|
||||
# ruff: noqa: PLR0913
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
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
|
||||
from core.services import jwt_token
|
||||
|
||||
User = get_user_model()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseJWTAuthentication(authentication.BaseAuthentication):
|
||||
"""Base JWT authentication class."""
|
||||
class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
"""JWT authentication for application-delegated API access.
|
||||
|
||||
def __init__(
|
||||
self, secret_key, algorithm, issuer, audience, expiration_seconds, token_type
|
||||
):
|
||||
"""Initialize the JWT authentication backend with the given token service configuration.
|
||||
|
||||
Args:
|
||||
secret_key: Secret key for JWT encoding/decoding
|
||||
algorithm: JWT algorithm (e.g. HS256)
|
||||
issuer: Expected token issuer identifier
|
||||
audience: Expected token audience identifier
|
||||
expiration_seconds: Token expiration time in seconds
|
||||
token_type: Token type (e.g. Bearer)
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._token_service = jwt_token.JwtTokenService(
|
||||
secret_key=secret_key,
|
||||
algorithm=algorithm,
|
||||
issuer=issuer,
|
||||
audience=audience,
|
||||
expiration_seconds=expiration_seconds,
|
||||
token_type=token_type,
|
||||
)
|
||||
Validates JWT tokens issued to applications that are acting on behalf
|
||||
of users. Tokens must include user_id, client_id, and delegation flag.
|
||||
"""
|
||||
|
||||
def authenticate(self, request):
|
||||
"""Extract and validate JWT from Authorization header.
|
||||
@@ -72,78 +48,6 @@ class BaseJWTAuthentication(authentication.BaseAuthentication):
|
||||
|
||||
return self.authenticate_credentials(token)
|
||||
|
||||
def decode_jwt(self, token):
|
||||
"""Decode and validate JWT token.
|
||||
|
||||
Args:
|
||||
token: JWT token string
|
||||
|
||||
Returns:
|
||||
Decoded payload dict, or None if token is invalid
|
||||
|
||||
Raises:
|
||||
AuthenticationFailed: If token is expired or has invalid issuer/audience
|
||||
"""
|
||||
|
||||
try:
|
||||
payload = self._token_service.decode_jwt(token)
|
||||
return payload
|
||||
except jwt_token.TokenExpiredError as e:
|
||||
logger.warning("Token expired")
|
||||
raise exceptions.AuthenticationFailed("Token expired.") from e
|
||||
except jwt_token.TokenInvalidError as e:
|
||||
logger.warning("Invalid JWT issuer or audience: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
except jwt_token.TokenDecodeError:
|
||||
# Invalid JWT token - defer to next authentication backend
|
||||
return None
|
||||
|
||||
def validate_payload(self, payload):
|
||||
"""Validate JWT payload claims.
|
||||
|
||||
Override in subclasses to add custom validation.
|
||||
|
||||
Args:
|
||||
payload: Decoded JWT payload
|
||||
|
||||
Raises:
|
||||
AuthenticationFailed: If required claims are missing or invalid
|
||||
"""
|
||||
|
||||
def get_user(self, payload):
|
||||
"""Retrieve and validate user from payload.
|
||||
|
||||
Args:
|
||||
payload: Decoded JWT payload
|
||||
|
||||
Returns:
|
||||
User instance
|
||||
|
||||
Raises:
|
||||
AuthenticationFailed: If user not found or inactive
|
||||
"""
|
||||
user_id = payload.get("user_id")
|
||||
|
||||
if not user_id:
|
||||
logger.warning("Missing 'user_id' in JWT payload")
|
||||
raise exceptions.AuthenticationFailed("Invalid token claims.")
|
||||
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist as e:
|
||||
logger.warning("User not found: %s", user_id)
|
||||
raise exceptions.AuthenticationFailed("User not found.") from e
|
||||
|
||||
if not user.is_active:
|
||||
logger.warning("Inactive user attempted authentication: %s", user_id)
|
||||
raise exceptions.AuthenticationFailed("User account is disabled.")
|
||||
|
||||
return user
|
||||
|
||||
def authenticate_header(self, request):
|
||||
"""Return authentication scheme for WWW-Authenticate header."""
|
||||
return "Bearer"
|
||||
|
||||
def authenticate_credentials(self, token):
|
||||
"""Validate JWT token and return authenticated user.
|
||||
|
||||
@@ -158,41 +62,36 @@ class BaseJWTAuthentication(authentication.BaseAuthentication):
|
||||
Raises:
|
||||
AuthenticationFailed: If token is expired, or user not found
|
||||
"""
|
||||
|
||||
payload = self.decode_jwt(token)
|
||||
|
||||
if payload is None:
|
||||
# Decode and validate JWT
|
||||
try:
|
||||
payload = pyJwt.decode(
|
||||
token,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithms=[settings.APPLICATION_JWT_ALG],
|
||||
issuer=settings.APPLICATION_JWT_ISSUER,
|
||||
audience=settings.APPLICATION_JWT_AUDIENCE,
|
||||
)
|
||||
except pyJwt.ExpiredSignatureError as e:
|
||||
logger.warning("Token expired")
|
||||
raise exceptions.AuthenticationFailed("Token expired.") from e
|
||||
except pyJwt.InvalidIssuerError as e:
|
||||
logger.warning("Invalid JWT issuer: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
except pyJwt.InvalidAudienceError as e:
|
||||
logger.warning("Invalid JWT audience: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
except pyJwt.InvalidTokenError:
|
||||
# Invalid JWT token - defer to next authentication backend
|
||||
return None
|
||||
|
||||
self.validate_payload(payload)
|
||||
user = self.get_user(payload)
|
||||
|
||||
return (user, payload)
|
||||
|
||||
|
||||
class ApplicationJWTAuthentication(BaseJWTAuthentication):
|
||||
"""JWT authentication for application-delegated API access.
|
||||
|
||||
Validates JWT tokens issued to applications that are acting on behalf
|
||||
of users. Tokens must include user_id, client_id, and delegation flag.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize authentication backend with application JWT settings from Django settings."""
|
||||
super().__init__(
|
||||
secret_key=settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithm=settings.APPLICATION_JWT_ALG,
|
||||
issuer=settings.APPLICATION_JWT_ISSUER,
|
||||
audience=settings.APPLICATION_JWT_AUDIENCE,
|
||||
expiration_seconds=settings.APPLICATION_JWT_EXPIRATION_SECONDS,
|
||||
token_type=settings.APPLICATION_JWT_TOKEN_TYPE,
|
||||
)
|
||||
|
||||
def validate_payload(self, payload):
|
||||
"""Validate application-specific claims."""
|
||||
user_id = payload.get("user_id")
|
||||
client_id = payload.get("client_id")
|
||||
is_delegated = payload.get("delegated", False)
|
||||
|
||||
if not user_id:
|
||||
logger.warning("Missing 'user_id' in JWT payload")
|
||||
raise exceptions.AuthenticationFailed("Invalid token claims.")
|
||||
|
||||
if not client_id:
|
||||
logger.warning("Missing 'client_id' in JWT payload")
|
||||
raise exceptions.AuthenticationFailed("Invalid token claims.")
|
||||
@@ -213,6 +112,22 @@ class ApplicationJWTAuthentication(BaseJWTAuthentication):
|
||||
logger.warning("Token is not marked as delegated")
|
||||
raise exceptions.AuthenticationFailed("Invalid token type.")
|
||||
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist as e:
|
||||
logger.warning("User not found: %s", user_id)
|
||||
raise exceptions.AuthenticationFailed("User not found.") from e
|
||||
|
||||
if not user.is_active:
|
||||
logger.warning("Inactive user attempted authentication: %s", user_id)
|
||||
raise exceptions.AuthenticationFailed("User account is disabled.")
|
||||
|
||||
return (user, payload)
|
||||
|
||||
def authenticate_header(self, request):
|
||||
"""Return authentication scheme for WWW-Authenticate header."""
|
||||
return "Bearer"
|
||||
|
||||
|
||||
class ResourceServerBackend(LaSuiteBackend):
|
||||
"""OIDC Resource Server backend for user creation and retrieval."""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""External API endpoints"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
@@ -7,6 +8,7 @@ from django.contrib.auth.hashers import check_password
|
||||
from django.core.exceptions import SuspiciousOperation, ValidationError
|
||||
from django.core.validators import validate_email
|
||||
|
||||
import jwt
|
||||
from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication
|
||||
from rest_framework import decorators, mixins, viewsets
|
||||
from rest_framework import (
|
||||
@@ -20,7 +22,6 @@ from rest_framework import (
|
||||
)
|
||||
|
||||
from core import api, models
|
||||
from core.services.jwt_token import JwtTokenService
|
||||
|
||||
from . import authentication, permissions, serializers
|
||||
|
||||
@@ -127,28 +128,33 @@ class ApplicationViewSet(viewsets.ViewSet):
|
||||
"Multiple user accounts share a common email."
|
||||
) from e
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
scope = " ".join(application.scopes or [])
|
||||
|
||||
token_service = JwtTokenService(
|
||||
secret_key=settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithm=settings.APPLICATION_JWT_ALG,
|
||||
issuer=settings.APPLICATION_JWT_ISSUER,
|
||||
audience=settings.APPLICATION_JWT_AUDIENCE,
|
||||
expiration_seconds=settings.APPLICATION_JWT_EXPIRATION_SECONDS,
|
||||
token_type=settings.APPLICATION_JWT_TOKEN_TYPE,
|
||||
)
|
||||
payload = {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"iat": now,
|
||||
"exp": now + timedelta(seconds=settings.APPLICATION_JWT_EXPIRATION_SECONDS),
|
||||
"client_id": client_id,
|
||||
"scope": scope,
|
||||
"user_id": str(user.id),
|
||||
"delegated": True,
|
||||
}
|
||||
|
||||
data = token_service.generate_jwt(
|
||||
user,
|
||||
scope,
|
||||
{
|
||||
"client_id": client_id,
|
||||
"delegated": True,
|
||||
},
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithm=settings.APPLICATION_JWT_ALG,
|
||||
)
|
||||
|
||||
return drf_response.Response(
|
||||
data,
|
||||
{
|
||||
"access_token": token,
|
||||
"token_type": settings.APPLICATION_JWT_TOKEN_TYPE,
|
||||
"expires_in": settings.APPLICATION_JWT_EXPIRATION_SECONDS,
|
||||
"scope": scope,
|
||||
},
|
||||
status=drf_status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
"""JWT token service."""
|
||||
|
||||
# pylint: disable=R0913,R0917
|
||||
# ruff: noqa: PLR0913
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
import jwt
|
||||
|
||||
|
||||
class JWTError(Exception):
|
||||
"""Base exception for all JWT token errors."""
|
||||
|
||||
|
||||
class TokenExpiredError(JWTError):
|
||||
"""Raised when the JWT token has expired."""
|
||||
|
||||
|
||||
class TokenInvalidError(JWTError):
|
||||
"""Raised when the JWT token has an invalid issuer or audience."""
|
||||
|
||||
|
||||
class TokenDecodeError(JWTError):
|
||||
"""Raised for any other unrecoverable JWT decode failure."""
|
||||
|
||||
|
||||
class JwtTokenService:
|
||||
"""Generic JWT token service with configurable settings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
secret_key: str,
|
||||
algorithm: str,
|
||||
issuer: str,
|
||||
audience: str,
|
||||
expiration_seconds: int,
|
||||
token_type: str,
|
||||
):
|
||||
"""
|
||||
Initialize the token service with custom settings.
|
||||
|
||||
Args:
|
||||
secret_key: Secret key for JWT encoding/decoding
|
||||
algorithm: JWT algorithm
|
||||
issuer: Token issuer identifier
|
||||
audience: Token audience identifier
|
||||
expiration_seconds: Token expiration time in seconds
|
||||
token_type: Token type
|
||||
|
||||
Raises:
|
||||
ImproperlyConfigured: If secret_key is None or empty
|
||||
"""
|
||||
if not secret_key:
|
||||
raise ImproperlyConfigured("Secret key is required.")
|
||||
if not algorithm:
|
||||
raise ImproperlyConfigured("Algorithm is required.")
|
||||
if not token_type:
|
||||
raise ImproperlyConfigured("Token's type is required.")
|
||||
if expiration_seconds is None:
|
||||
raise ImproperlyConfigured("Expiration's seconds is required.")
|
||||
|
||||
self._key = secret_key
|
||||
self._algorithm = algorithm
|
||||
self._issuer = issuer
|
||||
self._audience = audience
|
||||
self._expiration_seconds = expiration_seconds
|
||||
self._token_type = token_type
|
||||
|
||||
def generate_jwt(
|
||||
self, user, scope: str, extra_payload: Optional[dict] = None
|
||||
) -> dict:
|
||||
"""
|
||||
Generate an access token for the given user.
|
||||
|
||||
Note: any extra_payload variables named iat, exp, or user_id will
|
||||
be overwritten by this service
|
||||
|
||||
Args:
|
||||
user: User instance for whom to generate the token
|
||||
scope: Space-separated scope string
|
||||
|
||||
Returns:
|
||||
Dictionary containing access_token, token_type, expires_in, and scope optionally
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
payload = extra_payload.copy() if extra_payload else {}
|
||||
|
||||
payload.update(
|
||||
{
|
||||
"iat": now,
|
||||
"exp": now + timedelta(seconds=self._expiration_seconds),
|
||||
"user_id": str(user.id),
|
||||
}
|
||||
)
|
||||
|
||||
if self._issuer:
|
||||
payload["iss"] = self._issuer
|
||||
if self._audience:
|
||||
payload["aud"] = self._audience
|
||||
if scope:
|
||||
payload["scope"] = scope
|
||||
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
self._key,
|
||||
algorithm=self._algorithm,
|
||||
)
|
||||
|
||||
response = {
|
||||
"access_token": token,
|
||||
"token_type": self._token_type,
|
||||
"expires_in": self._expiration_seconds,
|
||||
}
|
||||
|
||||
if scope:
|
||||
response["scope"] = scope
|
||||
|
||||
return response
|
||||
|
||||
def decode_jwt(self, token):
|
||||
"""Decode and validate JWT token.
|
||||
|
||||
Args:
|
||||
token: JWT token string
|
||||
|
||||
Returns:
|
||||
Decoded payload dict.
|
||||
|
||||
Raises:
|
||||
TokenExpiredError: If the token has expired.
|
||||
TokenInvalidError: If the token has an invalid issuer or audience.
|
||||
TokenDecodeError: If the token is malformed or cannot be decoded.
|
||||
"""
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
self._key,
|
||||
algorithms=[self._algorithm],
|
||||
issuer=self._issuer,
|
||||
audience=self._audience,
|
||||
)
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError as e:
|
||||
raise TokenExpiredError("Token expired.") from e
|
||||
except (jwt.InvalidIssuerError, jwt.InvalidAudienceError) as e:
|
||||
raise TokenInvalidError("Invalid token.") from e
|
||||
except jwt.InvalidTokenError as e:
|
||||
raise TokenDecodeError("Token decode error.") from e
|
||||
@@ -55,8 +55,9 @@ def test_api_rooms_list_requires_authentication():
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_api_rooms_list_inactive_user():
|
||||
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)])
|
||||
@@ -71,9 +72,11 @@ def test_api_rooms_list_inactive_user():
|
||||
assert "user account is disabled" in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_list_with_valid_token():
|
||||
def test_api_rooms_list_with_valid_token(settings):
|
||||
"""Listing rooms with valid token should succeed."""
|
||||
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
@@ -89,8 +92,9 @@ def test_api_rooms_list_with_valid_token():
|
||||
assert response.data["results"][0]["id"] == str(room.id)
|
||||
|
||||
|
||||
def test_api_rooms_list_with_no_rooms():
|
||||
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()
|
||||
|
||||
@@ -108,6 +112,7 @@ def test_api_rooms_list_with_no_rooms():
|
||||
|
||||
def test_api_rooms_list_with_expired_token(settings):
|
||||
"""Listing rooms with expired token should return 401."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
settings.APPLICATION_JWT_EXPIRATION_SECONDS = 0
|
||||
|
||||
user = UserFactory()
|
||||
@@ -148,8 +153,9 @@ def test_api_rooms_list_with_invalid_rs_token(settings):
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_api_rooms_list_missing_scope():
|
||||
def test_api_rooms_list_missing_scope(settings):
|
||||
"""Listing rooms without required scope should return 403."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
@@ -167,8 +173,9 @@ def test_api_rooms_list_missing_scope():
|
||||
)
|
||||
|
||||
|
||||
def test_api_rooms_list_no_scope():
|
||||
def test_api_rooms_list_no_scope(settings):
|
||||
"""Listing rooms without any scope should return 403."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
@@ -183,8 +190,9 @@ def test_api_rooms_list_no_scope():
|
||||
assert "insufficient permissions." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_list_filters_by_user():
|
||||
def test_api_rooms_list_filters_by_user(settings):
|
||||
"""List should only return rooms accessible to the authenticated user."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user1 = UserFactory()
|
||||
user2 = UserFactory()
|
||||
@@ -209,9 +217,11 @@ def test_api_rooms_list_filters_by_user():
|
||||
assert str(room2.id) not in returned_ids
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_requires_authentication():
|
||||
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)])
|
||||
|
||||
@@ -221,8 +231,9 @@ def test_api_rooms_retrieve_requires_authentication():
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_inactive_user():
|
||||
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)])
|
||||
@@ -239,6 +250,7 @@ def test_api_rooms_retrieve_inactive_user():
|
||||
|
||||
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()
|
||||
@@ -283,8 +295,9 @@ def test_api_rooms_retrieve_with_invalid_rs_token(settings):
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_requires_scope():
|
||||
def test_api_rooms_retrieve_requires_scope(settings):
|
||||
"""Retrieving a room requires ROOMS_RETRIEVE scope."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
@@ -302,8 +315,9 @@ def test_api_rooms_retrieve_requires_scope():
|
||||
)
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_no_scope():
|
||||
def test_api_rooms_retrieve_no_scope(settings):
|
||||
"""Retrieving rooms without any scope should return 403."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
@@ -321,6 +335,7 @@ def test_api_rooms_retrieve_no_scope():
|
||||
|
||||
def test_api_rooms_retrieve_success(settings):
|
||||
"""Retrieving a room with correct scope should succeed."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
settings.APPLICATION_BASE_URL = "http://your-application.com"
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
settings.ROOM_TELEPHONY_PHONE_NUMBER = "+1-555-0100"
|
||||
@@ -352,8 +367,9 @@ def test_api_rooms_retrieve_success(settings):
|
||||
}
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_success_by_user():
|
||||
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()
|
||||
@@ -392,8 +408,9 @@ def test_api_rooms_retrieve_success_by_user():
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_not_found():
|
||||
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])
|
||||
@@ -406,9 +423,11 @@ def test_api_rooms_retrieve_not_found():
|
||||
assert "no room matches the given query." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_create_requires_authentication():
|
||||
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/")
|
||||
|
||||
@@ -417,6 +436,7 @@ def test_api_rooms_create_requires_authentication():
|
||||
|
||||
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()
|
||||
@@ -457,8 +477,9 @@ def test_api_rooms_create_with_invalid_rs_token(settings):
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_api_rooms_create_inactive_user():
|
||||
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)
|
||||
|
||||
@@ -472,8 +493,9 @@ def test_api_rooms_create_inactive_user():
|
||||
assert "user account is disabled" in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_create_requires_scope():
|
||||
def test_api_rooms_create_requires_scope(settings):
|
||||
"""Creating a room requires ROOMS_CREATE scope."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory()
|
||||
|
||||
# Token without ROOMS_CREATE scope
|
||||
@@ -490,8 +512,9 @@ def test_api_rooms_create_requires_scope():
|
||||
)
|
||||
|
||||
|
||||
def test_api_rooms_create_no_scope():
|
||||
def test_api_rooms_create_no_scope(settings):
|
||||
"""Creating rooms without any scope should return 403."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
@@ -506,8 +529,9 @@ def test_api_rooms_create_no_scope():
|
||||
assert "insufficient permissions." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_create_success():
|
||||
def test_api_rooms_create_success(settings):
|
||||
"""Creating a room with correct scope should succeed."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
@@ -531,8 +555,9 @@ def test_api_rooms_create_success():
|
||||
assert room.access_level == "trusted"
|
||||
|
||||
|
||||
def test_api_rooms_create_readonly_enforcement():
|
||||
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()
|
||||
|
||||
@@ -565,8 +590,9 @@ def test_api_rooms_create_readonly_enforcement():
|
||||
assert room.access_level == "trusted"
|
||||
|
||||
|
||||
def test_api_rooms_unknown_actions():
|
||||
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)])
|
||||
@@ -597,6 +623,7 @@ def test_api_rooms_unknown_actions():
|
||||
|
||||
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"
|
||||
settings.APPLICATION_BASE_URL = None
|
||||
|
||||
user = UserFactory()
|
||||
@@ -615,6 +642,7 @@ def test_api_rooms_response_no_url(settings):
|
||||
|
||||
def test_api_rooms_response_no_telephony(settings):
|
||||
"""Response should not include telephony field when ROOM_TELEPHONY_ENABLED is False."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
settings.ROOM_TELEPHONY_ENABLED = False
|
||||
|
||||
user = UserFactory()
|
||||
@@ -633,6 +661,7 @@ def test_api_rooms_response_no_telephony(settings):
|
||||
|
||||
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()
|
||||
|
||||
@@ -664,6 +693,7 @@ def test_api_rooms_token_scope_case_insensitive(settings):
|
||||
|
||||
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()
|
||||
|
||||
@@ -696,6 +726,7 @@ def test_api_rooms_token_without_delegated_flag(settings):
|
||||
@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()
|
||||
|
||||
@@ -728,6 +759,7 @@ def test_api_rooms_token_invalid_signature(mock_rs_authenticate, settings):
|
||||
@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()
|
||||
|
||||
@@ -759,6 +791,7 @@ def test_api_rooms_token_invalid_alg(mock_rs_authenticate, settings):
|
||||
|
||||
def test_api_rooms_token_missing_client_id(settings):
|
||||
"""Token without client_id should be rejected."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -788,6 +821,7 @@ def test_api_rooms_token_missing_client_id(settings):
|
||||
|
||||
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)
|
||||
@@ -817,6 +851,7 @@ def test_api_rooms_token_missing_user_id(settings):
|
||||
|
||||
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()
|
||||
|
||||
@@ -847,6 +882,7 @@ def test_api_rooms_token_invalid_audience(settings):
|
||||
|
||||
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)
|
||||
@@ -876,6 +912,7 @@ def test_api_rooms_token_unknown_user(settings):
|
||||
|
||||
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 = {
|
||||
@@ -904,6 +941,7 @@ def test_api_rooms_token_unknown_application(settings):
|
||||
|
||||
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)
|
||||
|
||||
@@ -21,6 +21,7 @@ pytestmark = pytest.mark.django_db
|
||||
|
||||
def test_api_applications_generate_token_success(settings):
|
||||
"""Valid credentials should return a JWT token."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
UserFactory(email="User.Family@example.com")
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
@@ -172,8 +173,9 @@ def test_api_applications_generate_token_domain_not_authorized():
|
||||
assert "not authorized for this email domain" in str(response.data)
|
||||
|
||||
|
||||
def test_api_applications_generate_token_domain_authorized():
|
||||
def test_api_applications_generate_token_domain_authorized(settings):
|
||||
"""Application with domain authorization should succeed."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory(email="user@allowed.com")
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
@@ -228,6 +230,7 @@ def test_api_applications_generate_token_user_not_found():
|
||||
@freeze_time("2023-01-15 12:00:00")
|
||||
def test_api_applications_token_payload_structure(settings):
|
||||
"""Generated token should have correct payload structure."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory(email="user@example.com")
|
||||
|
||||
application = ApplicationFactory(
|
||||
@@ -277,6 +280,7 @@ def test_api_applications_token_payload_structure(settings):
|
||||
def test_api_applications_token_new_user(settings):
|
||||
"""Should create a new pending user when creation is allowed and user doesn't exist."""
|
||||
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
settings.APPLICATION_ALLOW_USER_CREATION = True
|
||||
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = True
|
||||
settings.OIDC_USER_SUB_FIELD_IMMUTABLE = False
|
||||
@@ -333,6 +337,7 @@ def test_api_applications_token_new_user(settings):
|
||||
def test_api_applications_token_existing_user(settings):
|
||||
"""Application should not create a new user when user exist."""
|
||||
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory(email="user@example.com")
|
||||
|
||||
settings.APPLICATION_ALLOW_USER_CREATION = True
|
||||
|
||||
@@ -667,7 +667,7 @@ class Base(Configuration):
|
||||
[],
|
||||
environ_name="BREVO_API_CONTACT_LIST_IDS",
|
||||
environ_prefix=None,
|
||||
converter=int,
|
||||
converter=lambda x: int(x), # pylint: disable=unnecessary-lambda
|
||||
)
|
||||
BREVO_API_CONTACT_ATTRIBUTES = values.DictValue({"VISIO_USER": True})
|
||||
BREVO_API_TIMEOUT = values.PositiveIntegerValue(
|
||||
@@ -917,9 +917,6 @@ class Test(Base):
|
||||
USE_SWAGGER = True
|
||||
EXTERNAL_API_ENABLED = True
|
||||
|
||||
APPLICATION_JWT_SECRET_KEY = "devKey" # noqa:S105
|
||||
APPLICATION_JWT_AUDIENCE = "Test inc."
|
||||
|
||||
CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True)
|
||||
|
||||
def __init__(self):
|
||||
|
||||
+22
-22
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.8.0"
|
||||
version = "1.6.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,15 +25,15 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"boto3==1.42.49",
|
||||
"boto3==1.40.69",
|
||||
"Brotli==1.2.0",
|
||||
"brevo-python==1.2.0",
|
||||
"celery[redis]==5.6.2",
|
||||
"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.2.0",
|
||||
"django-lasuite[all]==0.0.24",
|
||||
"django-countries==8.0.0",
|
||||
"django-lasuite[all]==0.0.19",
|
||||
"django-parler==2.3",
|
||||
"redis==5.2.1",
|
||||
"django-redis==6.0.0",
|
||||
@@ -42,21 +42,21 @@ dependencies = [
|
||||
"django==5.2.11",
|
||||
"djangorestframework==3.16.1",
|
||||
"drf_spectacular==0.29.0",
|
||||
"dockerflow==2026.1.26",
|
||||
"dockerflow==2024.4.2",
|
||||
"easy_thumbnails==2.10.1",
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==25.1.0",
|
||||
"jsonschema==4.26.0",
|
||||
"markdown==3.10.2",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.25.1",
|
||||
"markdown==3.10",
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"psycopg[binary]==3.3.2",
|
||||
"PyJWT==2.11.0",
|
||||
"psycopg[binary]==3.2.12",
|
||||
"PyJWT==2.10.1",
|
||||
"python-frontmatter==1.1.0",
|
||||
"requests==2.32.5",
|
||||
"sentry-sdk==2.53.0",
|
||||
"sentry-sdk==2.43.0",
|
||||
"whitenoise==6.11.0",
|
||||
"mozilla-django-oidc==5.0.2",
|
||||
"livekit-api==1.1.0",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"livekit-api==1.0.7",
|
||||
"aiohttp==3.13.3",
|
||||
]
|
||||
|
||||
@@ -69,21 +69,21 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"drf-spectacular-sidecar==2026.1.1",
|
||||
"drf-spectacular-sidecar==2025.10.1",
|
||||
"freezegun==1.5.5",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.10.0",
|
||||
"pyfakefs==6.1.1",
|
||||
"pylint-django==2.7.0",
|
||||
"ipython==9.7.0",
|
||||
"pyfakefs==5.10.2",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint<4.0.0",
|
||||
"pytest-cov==7.0.0",
|
||||
"pytest-django==4.12.0",
|
||||
"pytest==9.0.2",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==9.0.0",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.8.0",
|
||||
"responses==0.25.8",
|
||||
"ruff==0.15.1",
|
||||
"types-requests==2.32.4.20260107",
|
||||
"ruff==0.14.4",
|
||||
"types-requests==2.32.4.20250913",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
|
||||
@@ -43,8 +43,7 @@ RUN apk update && apk upgrade libssl3 \
|
||||
libxml2>=2.12.7-r2 \
|
||||
libxslt>=1.1.39-r2 \
|
||||
libexpat>=2.7.2-r0 \
|
||||
libpng>=1.6.53-r0 \
|
||||
&& apk del curl
|
||||
libpng>=1.6.53-r0
|
||||
|
||||
USER nginx
|
||||
|
||||
|
||||
@@ -5,12 +5,6 @@ server {
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location = /.well-known/windows-app-web-link {
|
||||
default_type application/json;
|
||||
alias /usr/share/nginx/html/.well-known/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
+957
-2918
File diff suppressed because it is too large
Load Diff
+24
-24
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.8.0",
|
||||
"version": "1.6.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -13,56 +13,56 @@
|
||||
"check": "prettier --check ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.34",
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.30",
|
||||
"@fontsource/material-icons-outlined": "5.2.6",
|
||||
"@livekit/components-react": "2.9.19",
|
||||
"@livekit/components-styles": "1.2.0",
|
||||
"@livekit/track-processors": "0.7.0",
|
||||
"@pandacss/preset-panda": "1.8.2",
|
||||
"@react-aria/toast": "3.0.10",
|
||||
"@react-types/overlays": "3.9.3",
|
||||
"@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",
|
||||
"@remixicon/react": "4.6.0",
|
||||
"@tanstack/react-query": "5.90.21",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.0.27",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"derive-valtio": "0.2.0",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.2",
|
||||
"i18next": "25.8.8",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"humanize-duration": "3.33.0",
|
||||
"i18next": "25.3.1",
|
||||
"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.17.1",
|
||||
"posthog-js": "1.342.1",
|
||||
"livekit-client": "2.15.7",
|
||||
"posthog-js": "1.256.2",
|
||||
"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.3.0",
|
||||
"wouter": "3.9.0"
|
||||
"valtio": "2.1.5",
|
||||
"wouter": "3.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pandacss/dev": "1.8.2",
|
||||
"@tanstack/eslint-plugin-query": "5.91.4",
|
||||
"@tanstack/react-query-devtools": "5.91.3",
|
||||
"@pandacss/dev": "0.54.0",
|
||||
"@tanstack/eslint-plugin-query": "5.81.2",
|
||||
"@tanstack/react-query-devtools": "5.81.5",
|
||||
"@types/humanize-duration": "3.27.4",
|
||||
"@types/node": "22.16.0",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.35.1",
|
||||
"@typescript-eslint/parser": "8.35.1",
|
||||
"@vitejs/plugin-react": "5.1.4",
|
||||
"@vitejs/plugin-react": "4.6.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.8.1",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.3.1",
|
||||
"vite-tsconfig-paths": "6.1.1"
|
||||
"vite": "7.0.8",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[
|
||||
{
|
||||
"packageFamilyName" : "Visio_g3z6ba6vek6vg",
|
||||
"paths" : [ "*" ]
|
||||
}
|
||||
]
|
||||
+3
-1
@@ -11,7 +11,9 @@ import {
|
||||
ProcessorType,
|
||||
} from '.'
|
||||
|
||||
export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface {
|
||||
export class UnifiedBackgroundTrackProcessor
|
||||
implements BackgroundProcessorInterface
|
||||
{
|
||||
processor: ProcessorWrapper<BackgroundOptions>
|
||||
opts: BackgroundOptions
|
||||
processorType: ProcessorType
|
||||
|
||||
@@ -13,7 +13,8 @@ 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
|
||||
|
||||
-6
@@ -6,7 +6,6 @@ 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,
|
||||
@@ -28,11 +27,6 @@ export const ParticipantsToggle = ({
|
||||
|
||||
const tooltipLabel = isParticipantsOpen ? 'open' : 'closed'
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'toggle-participants',
|
||||
handler: toggleParticipants,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiEmotionLine } from '@remixicon/react'
|
||||
import { useState, useRef } from 'react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { ToggleButton, Button } from '@/primitives'
|
||||
@@ -12,12 +12,7 @@ import {
|
||||
} from '@/features/rooms/livekit/components/ReactionPortal'
|
||||
import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
import {
|
||||
Popover as RACPopover,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
} from 'react-aria-components'
|
||||
import { FocusScope } from '@react-aria/focus'
|
||||
import { Toolbar as RACToolbar } from 'react-aria-components'
|
||||
import { Participant } from 'livekit-client'
|
||||
import useRateLimiter from '@/hooks/useRateLimiter'
|
||||
|
||||
@@ -45,11 +40,11 @@ export const ReactionsToggle = () => {
|
||||
const instanceIdRef = useRef(0)
|
||||
const room = useRoomContext()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'reaction',
|
||||
handler: () => setIsOpen((prev) => !prev),
|
||||
handler: () => setIsVisible((prev) => !prev),
|
||||
})
|
||||
|
||||
const sendReaction = async (emoji: string) => {
|
||||
@@ -84,76 +79,100 @@ export const ReactionsToggle = () => {
|
||||
windowMs: 1000,
|
||||
})
|
||||
|
||||
// Custom animation implementation for the emoji toolbar
|
||||
// Could not use a menu and its animation, because a menu would make the toolbar inaccessible by keyboard
|
||||
// animation isn't perfect
|
||||
const [isRendered, setIsRendered] = useState(isVisible)
|
||||
const [opacity, setOpacity] = useState(isVisible ? 1 : 0)
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
// Show: first render, then animate in
|
||||
setIsRendered(true)
|
||||
// Need to delay setting opacity to ensure CSS transition works
|
||||
// (using requestAnimationFrame to ensure DOM has updated)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setOpacity(1)
|
||||
})
|
||||
})
|
||||
} else if (isRendered) {
|
||||
// Hide: first animate out, then unrender
|
||||
setOpacity(0)
|
||||
|
||||
// Wait for animation to complete before removing from DOM
|
||||
const timer = setTimeout(() => {
|
||||
setIsRendered(false)
|
||||
}, 200) // Match this to your animation duration
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isVisible, isRendered])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={css({ position: 'relative' })}>
|
||||
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<ToggleButton
|
||||
square
|
||||
variant="primaryDark"
|
||||
aria-label={t('button')}
|
||||
tooltip={t('button')}
|
||||
isSelected={isOpen}
|
||||
onChange={setIsOpen}
|
||||
>
|
||||
<RiEmotionLine />
|
||||
</ToggleButton>
|
||||
<RACPopover
|
||||
placement="top"
|
||||
offset={8}
|
||||
isNonModal
|
||||
shouldCloseOnInteractOutside={() => false}
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
})}
|
||||
>
|
||||
<ToggleButton
|
||||
square
|
||||
variant="primaryDark"
|
||||
aria-label={t('button')}
|
||||
tooltip={t('button')}
|
||||
onPress={() => setIsVisible(!isVisible)}
|
||||
>
|
||||
<RiEmotionLine />
|
||||
</ToggleButton>
|
||||
{isRendered && (
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: -63,
|
||||
left: -162,
|
||||
borderRadius: '8px',
|
||||
padding: '0.35rem',
|
||||
backgroundColor: 'primaryDark.50',
|
||||
'&[data-entering]': {
|
||||
animation: 'fade 200ms ease',
|
||||
},
|
||||
'&[data-exiting]': {
|
||||
animation: 'fade 200ms ease-in reverse',
|
||||
},
|
||||
opacity: opacity,
|
||||
transition: 'opacity 0.2s ease',
|
||||
})}
|
||||
onTransitionEnd={() => {
|
||||
if (!isVisible) {
|
||||
setIsRendered(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog className={css({ outline: 'none' })}>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-autofocus -- FocusScope autoFocus is programmatic focus for overlays, not the HTML autofocus attribute */}
|
||||
<FocusScope contain autoFocus restoreFocus>
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-orientation="horizontal"
|
||||
aria-label={t('button')}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
<RACToolbar
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
>
|
||||
{Object.values(Emoji).map((emoji, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onPress={() => debouncedSendReaction(emoji)}
|
||||
aria-label={t('send', { emoji: getEmojiLabel(emoji, t) })}
|
||||
variant="primaryTextDark"
|
||||
size="sm"
|
||||
square
|
||||
data-attr={`send-reaction-${emoji}`}
|
||||
>
|
||||
{Object.values(Emoji).map((emoji, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onPress={() => debouncedSendReaction(emoji)}
|
||||
aria-label={t('send', { emoji: getEmojiLabel(emoji, t) })}
|
||||
variant="primaryTextDark"
|
||||
size="sm"
|
||||
square
|
||||
data-attr={`send-reaction-${emoji}`}
|
||||
>
|
||||
<img
|
||||
src={`/assets/reactions/${emoji}.png`}
|
||||
alt=""
|
||||
className={css({
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</FocusScope>
|
||||
</Dialog>
|
||||
</RACPopover>
|
||||
</DialogTrigger>
|
||||
<img
|
||||
src={`/assets/reactions/${emoji}.png`}
|
||||
alt=""
|
||||
className={css({
|
||||
minHeight: '28px',
|
||||
minWidth: '28px',
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
))}
|
||||
</RACToolbar>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ReactionPortals reactions={reactions} />
|
||||
</>
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
import React from 'react'
|
||||
|
||||
/** @public */
|
||||
export interface AllowMediaPlaybackProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
export interface AllowMediaPlaybackProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
label?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ 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,8 +13,7 @@ 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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ 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
|
||||
|
||||
@@ -66,7 +66,8 @@ const LayoutWrapper = styled(
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
export interface VideoConferenceProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** @alpha */
|
||||
SettingsComponent?: React.ComponentType
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
RiSpeakerLine,
|
||||
RiVideoOnLine,
|
||||
RiEyeLine,
|
||||
RiKeyboardBoxLine,
|
||||
} from '@remixicon/react'
|
||||
import { AccountTab } from './tabs/AccountTab'
|
||||
import { NotificationsTab } from './tabs/NotificationsTab'
|
||||
@@ -20,12 +19,11 @@ import { GeneralTab } from './tabs/GeneralTab'
|
||||
import { AudioTab } from './tabs/AudioTab'
|
||||
import { VideoTab } from './tabs/VideoTab'
|
||||
import { TranscriptionTab } from './tabs/TranscriptionTab'
|
||||
import { ShortcutTab } from './tabs/ShortcutTab'
|
||||
import { useRef } from 'react'
|
||||
import { useMediaQuery } from '@/features/rooms/livekit/hooks/useMediaQuery'
|
||||
import { SettingsDialogExtendedKey } from '@/features/settings/type'
|
||||
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
|
||||
import { AccessibilityTab } from './tabs/AccessibilityTab'
|
||||
import AccessibilityTab from './tabs/AccessibilityTab'
|
||||
|
||||
const tabsStyle = css({
|
||||
maxHeight: '40.625rem', // fixme size copied from meet settings modal
|
||||
@@ -41,7 +39,6 @@ const tabListContainerStyle = css({
|
||||
flexDirection: 'column',
|
||||
borderRight: '1px solid lightGray', // fixme poor color management
|
||||
paddingY: '1rem',
|
||||
paddingLeft: '0.2rem',
|
||||
paddingRight: '1.5rem',
|
||||
})
|
||||
|
||||
@@ -110,10 +107,6 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
|
||||
{isWideScreen &&
|
||||
t(`tabs.${SettingsDialogExtendedKey.NOTIFICATIONS}`)}
|
||||
</Tab>
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.SHORTCUTS}>
|
||||
<RiKeyboardBoxLine />
|
||||
{isWideScreen && t(`tabs.${SettingsDialogExtendedKey.SHORTCUTS}`)}
|
||||
</Tab>
|
||||
{isAdminOrOwner && (
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.TRANSCRIPTION}>
|
||||
<Icon type="symbols" name="speech_to_text" />
|
||||
@@ -137,7 +130,6 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
|
||||
<VideoTab id={SettingsDialogExtendedKey.VIDEO} />
|
||||
<GeneralTab id={SettingsDialogExtendedKey.GENERAL} />
|
||||
<NotificationsTab id={SettingsDialogExtendedKey.NOTIFICATIONS} />
|
||||
<ShortcutTab id={SettingsDialogExtendedKey.SHORTCUTS} />
|
||||
{/* Transcription tab won't be accessible if the tab is not active in the tab list */}
|
||||
<TranscriptionTab id={SettingsDialogExtendedKey.TRANSCRIPTION} />
|
||||
<AccessibilityTab id={SettingsDialogExtendedKey.ACCESSIBILITY} />
|
||||
|
||||
@@ -36,3 +36,5 @@ export const AccessibilityTab = ({ id }: AccessibilityTabProps) => {
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccessibilityTab
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { shortcutCatalog } from '@/features/shortcuts/catalog'
|
||||
import { ShortcutRow } from '@/features/shortcuts/components/ShortcutRow'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TabPanel, type TabPanelProps } from '@/primitives/Tabs'
|
||||
import { H } from '@/primitives'
|
||||
|
||||
const tableStyle = css({
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
overflowY: 'auto',
|
||||
'& th, & td': {
|
||||
padding: '0.65rem 0',
|
||||
textAlign: 'left',
|
||||
},
|
||||
'& tbody tr': {
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
},
|
||||
})
|
||||
|
||||
export const ShortcutTab = ({ id }: Pick<TabPanelProps, 'id'>) => {
|
||||
const { t } = useTranslation(['settings', 'rooms'])
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
id={id}
|
||||
padding="md"
|
||||
flex
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.75rem',
|
||||
})}
|
||||
>
|
||||
<H lvl={2}>{t('shortcuts.listLabel')}</H>
|
||||
<table className={tableStyle}>
|
||||
<thead className="sr-only">
|
||||
<tr>
|
||||
<th scope="col">{t('shortcuts.columnAction')}</th>
|
||||
<th scope="col">{t('shortcuts.columnShortcut')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{shortcutCatalog.map((item) => (
|
||||
<ShortcutRow key={item?.id} descriptor={item} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,5 @@ export enum SettingsDialogExtendedKey {
|
||||
GENERAL = 'general',
|
||||
NOTIFICATIONS = 'notifications',
|
||||
TRANSCRIPTION = 'transcription',
|
||||
SHORTCUTS = 'shortcuts',
|
||||
ACCESSIBILITY = 'accessibility',
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
|
||||
type ShortcutBadgeProps = {
|
||||
visualLabel: string
|
||||
srLabel?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const badgeStyle = css({
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
paddingInline: '0.4rem',
|
||||
paddingBlock: '0.2rem',
|
||||
borderRadius: '6px',
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: '5.5rem',
|
||||
textAlign: 'center',
|
||||
})
|
||||
|
||||
export const ShortcutBadge: React.FC<ShortcutBadgeProps> = ({
|
||||
visualLabel,
|
||||
srLabel,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className={cx(badgeStyle, className)} aria-hidden="true">
|
||||
<span>{visualLabel}</span>
|
||||
</div>
|
||||
{srLabel && <span className="sr-only">{srLabel}</span>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { text } from '@/primitives/Text'
|
||||
import { ShortcutDescriptor } from '../catalog'
|
||||
import { ShortcutBadge } from './ShortcutBadge'
|
||||
import { useShortcutFormatting } from '../hooks/useShortcutFormatting'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ShortcutRowProps = {
|
||||
descriptor: ShortcutDescriptor
|
||||
}
|
||||
|
||||
const shortcutCellStyle = css({
|
||||
textAlign: 'right',
|
||||
})
|
||||
|
||||
export const ShortcutRow: React.FC<ShortcutRowProps> = ({ descriptor }) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'shortcutsPanel' })
|
||||
const { formatVisual, formatForSR } = useShortcutFormatting()
|
||||
|
||||
const visualShortcut = formatVisual(
|
||||
descriptor.shortcut,
|
||||
descriptor.code,
|
||||
descriptor.kind
|
||||
)
|
||||
const srShortcut = formatForSR(
|
||||
descriptor.shortcut,
|
||||
descriptor.code,
|
||||
descriptor.kind
|
||||
)
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className={text({ variant: 'body' })}>
|
||||
{t(`actions.${descriptor.id}`)}
|
||||
</td>
|
||||
<td className={shortcutCellStyle}>
|
||||
<ShortcutBadge visualLabel={visualShortcut} srLabel={srShortcut} />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Shortcut } from './types'
|
||||
import { isMacintosh } from '@/utils/livekit'
|
||||
|
||||
// Visible label for a shortcut (uses ⌘/Ctrl prefix when needed).
|
||||
export const formatShortcutLabel = (shortcut?: Shortcut) => {
|
||||
if (!shortcut) return '—'
|
||||
const key = shortcut.key?.toUpperCase()
|
||||
if (!key) return '—'
|
||||
const parts: string[] = []
|
||||
if (shortcut.ctrlKey) parts.push(isMacintosh() ? '⌘' : 'Ctrl')
|
||||
if (shortcut.altKey) parts.push(isMacintosh() ? '⌥' : 'Alt')
|
||||
if (shortcut.shiftKey) parts.push('Shift')
|
||||
parts.push(key)
|
||||
return parts.join('+')
|
||||
}
|
||||
|
||||
// SR-friendly label for a shortcut (reads "Control plus D").
|
||||
export const formatShortcutLabelForSR = (
|
||||
shortcut: Shortcut | undefined,
|
||||
{
|
||||
controlLabel,
|
||||
commandLabel,
|
||||
altLabel,
|
||||
optionLabel,
|
||||
shiftLabel,
|
||||
plusLabel,
|
||||
noShortcutLabel,
|
||||
}: {
|
||||
controlLabel: string
|
||||
commandLabel: string
|
||||
altLabel: string
|
||||
optionLabel: string
|
||||
shiftLabel: string
|
||||
plusLabel: string
|
||||
noShortcutLabel: string
|
||||
}
|
||||
) => {
|
||||
if (!shortcut) return noShortcutLabel
|
||||
const key = shortcut.key?.toUpperCase()
|
||||
if (!key) return noShortcutLabel
|
||||
const ctrlWord = isMacintosh() ? commandLabel : controlLabel
|
||||
const altWord = isMacintosh() ? optionLabel : altLabel
|
||||
const parts: string[] = []
|
||||
if (shortcut.ctrlKey) parts.push(ctrlWord)
|
||||
if (shortcut.altKey) parts.push(altWord)
|
||||
if (shortcut.shiftKey) parts.push(shiftLabel)
|
||||
parts.push(key)
|
||||
return parts.join(` ${plusLabel} `)
|
||||
}
|
||||
|
||||
// Extract displayable key name from KeyboardEvent.code (ex: KeyV -> V).
|
||||
export const getKeyLabelFromCode = (code?: string) => {
|
||||
if (!code) return ''
|
||||
if (code.startsWith('Key') && code.length === 4) return code.slice(3)
|
||||
if (code.startsWith('Digit') && code.length === 6) return code.slice(5)
|
||||
if (code === 'Space') return '␣'
|
||||
if (code.startsWith('Arrow')) return code.slice(5) // Up, Down, Left, Right
|
||||
return code
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Shortcut } from '../types'
|
||||
import {
|
||||
formatShortcutLabel,
|
||||
formatShortcutLabelForSR,
|
||||
getKeyLabelFromCode,
|
||||
} from '../formatLabels'
|
||||
|
||||
export const useShortcutFormatting = () => {
|
||||
const { t } = useTranslation('rooms')
|
||||
|
||||
const formatVisual = useCallback(
|
||||
(shortcut?: Shortcut, code?: string, kind?: string) => {
|
||||
if (code && kind === 'longPress') {
|
||||
const label = getKeyLabelFromCode(code)
|
||||
return t('shortcutsPanel.visual.hold', { key: label || '?' })
|
||||
}
|
||||
return formatShortcutLabel(shortcut)
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
const formatForSR = useCallback(
|
||||
(shortcut?: Shortcut, code?: string, kind?: string) => {
|
||||
if (code && kind === 'longPress') {
|
||||
const label = getKeyLabelFromCode(code)
|
||||
return t('shortcutsPanel.sr.hold', { key: label || '?' })
|
||||
}
|
||||
return formatShortcutLabelForSR(shortcut, {
|
||||
controlLabel: t('shortcutsPanel.sr.control'),
|
||||
commandLabel: t('shortcutsPanel.sr.command'),
|
||||
altLabel: t('shortcutsPanel.sr.alt'),
|
||||
optionLabel: t('shortcutsPanel.sr.option'),
|
||||
shiftLabel: t('shortcutsPanel.sr.shift'),
|
||||
plusLabel: t('shortcutsPanel.sr.plus'),
|
||||
noShortcutLabel: t('shortcutsPanel.sr.noShortcut'),
|
||||
})
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
return {
|
||||
formatVisual,
|
||||
formatForSR,
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ export interface TranscriptionSegment {
|
||||
lastReceivedTime: number
|
||||
}
|
||||
|
||||
export interface TranscriptionSegmentWithParticipant extends TranscriptionSegment {
|
||||
export interface TranscriptionSegmentWithParticipant
|
||||
extends TranscriptionSegment {
|
||||
participant: Participant
|
||||
}
|
||||
|
||||
|
||||
@@ -595,41 +595,6 @@
|
||||
"muteParticipant": "{{name}} stummschalten",
|
||||
"fullScreen": "Vollbild"
|
||||
},
|
||||
"shortcutsPanel": {
|
||||
"title": "Tastenkombinationen",
|
||||
"categories": {
|
||||
"navigation": "Navigation",
|
||||
"media": "Medien",
|
||||
"interaction": "Interaktion"
|
||||
},
|
||||
"actions": {
|
||||
"open-shortcuts": "Tastenkürzel-Hilfe öffnen",
|
||||
"focus-toolbar": "Fokus auf die untere Symbolleiste",
|
||||
"toggle-microphone": "Mikrofon umschalten",
|
||||
"toggle-camera": "Kamera umschalten",
|
||||
"push-to-talk": "Push-to-talk (gedrückt halten zum Einschalten)",
|
||||
"reaction": "Reaktionspanel",
|
||||
"fullscreen": "Vollbild umschalten",
|
||||
"recording": "Aufnahmepanel umschalten",
|
||||
"raise-hand": "Hand heben oder senken",
|
||||
"toggle-chat": "Chat anzeigen/ausblenden",
|
||||
"toggle-participants": "Teilnehmer anzeigen/ausblenden",
|
||||
"open-shortcuts-settings": "Tastenkürzel-Einstellungen öffnen"
|
||||
},
|
||||
"sr": {
|
||||
"control": "Steuerung",
|
||||
"command": "Befehl",
|
||||
"alt": "Alt",
|
||||
"option": "Option",
|
||||
"shift": "Umschalt",
|
||||
"plus": "plus",
|
||||
"hold": "Halte {{key}} gedrückt",
|
||||
"noShortcut": "Kein Tastenkürzel"
|
||||
},
|
||||
"visual": {
|
||||
"hold": "Halte {{key}} gedrückt"
|
||||
}
|
||||
},
|
||||
"fullScreenWarning": {
|
||||
"message": "Um eine Endlosschleife zu vermeiden, teile nicht deinen gesamten Bildschirm. Teile stattdessen einen Tab oder ein anderes Fenster.",
|
||||
"stop": "Präsentation beenden",
|
||||
|
||||
@@ -100,11 +100,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"listLabel": "Tastenkürzel",
|
||||
"columnAction": "Aktion",
|
||||
"columnShortcut": "Tastenkürzel"
|
||||
},
|
||||
"dialog": {
|
||||
"heading": "Einstellungen"
|
||||
},
|
||||
@@ -125,7 +120,6 @@
|
||||
"general": "Allgemein",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"accessibility": "Barrierefreiheit",
|
||||
"transcription": "Transkription",
|
||||
"shortcuts": "Tastenkürzel"
|
||||
"transcription": "Transkription"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,41 +595,6 @@
|
||||
"muteParticipant": "Mute {{name}}",
|
||||
"fullScreen": "Full screen"
|
||||
},
|
||||
"shortcutsPanel": {
|
||||
"title": "Keyboard shortcuts",
|
||||
"categories": {
|
||||
"navigation": "Navigation",
|
||||
"media": "Media",
|
||||
"interaction": "Interaction"
|
||||
},
|
||||
"actions": {
|
||||
"open-shortcuts": "Open shortcuts help",
|
||||
"focus-toolbar": "Focus bottom toolbar",
|
||||
"toggle-microphone": "Toggle microphone",
|
||||
"toggle-camera": "Toggle camera",
|
||||
"push-to-talk": "Push-to-talk (hold to unmute)",
|
||||
"reaction": "Emoji reaction panel",
|
||||
"fullscreen": "Toggle fullscreen",
|
||||
"recording": "Toggle recording panel",
|
||||
"raise-hand": "Raise or lower hand",
|
||||
"toggle-chat": "Toggle chat",
|
||||
"toggle-participants": "Toggle participants",
|
||||
"open-shortcuts-settings": "Open shortcuts settings"
|
||||
},
|
||||
"sr": {
|
||||
"control": "Control",
|
||||
"command": "Command",
|
||||
"alt": "Alt",
|
||||
"option": "Option",
|
||||
"shift": "Shift",
|
||||
"plus": "plus",
|
||||
"hold": "Hold {{key}}",
|
||||
"noShortcut": "No shortcut"
|
||||
},
|
||||
"visual": {
|
||||
"hold": "Hold {{key}}"
|
||||
}
|
||||
},
|
||||
"fullScreenWarning": {
|
||||
"message": "To avoid infinite loop display, do not share your entire screen. Instead, share a tab or another window.",
|
||||
"stop": "Stop presenting",
|
||||
|
||||
@@ -100,11 +100,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"listLabel": "Keyboard shortcuts",
|
||||
"columnAction": "Action",
|
||||
"columnShortcut": "Shortcut"
|
||||
},
|
||||
"dialog": {
|
||||
"heading": "Settings"
|
||||
},
|
||||
@@ -125,7 +120,6 @@
|
||||
"general": "General",
|
||||
"notifications": "Notifications",
|
||||
"accessibility": "Accessibility",
|
||||
"transcription": "Transcription",
|
||||
"shortcuts": "Shortcuts"
|
||||
"transcription": "Transcription"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,41 +595,6 @@
|
||||
"muteParticipant": "Couper le micro de {{name}}",
|
||||
"fullScreen": "Plein écran"
|
||||
},
|
||||
"shortcutsPanel": {
|
||||
"title": "Raccourcis clavier",
|
||||
"categories": {
|
||||
"navigation": "Navigation",
|
||||
"media": "Média",
|
||||
"interaction": "Interaction"
|
||||
},
|
||||
"actions": {
|
||||
"open-shortcuts": "Ouvrir l’aide des raccourcis",
|
||||
"focus-toolbar": "Mettre le focus sur la barre d’outils du bas",
|
||||
"toggle-microphone": "Activer ou désactiver le micro",
|
||||
"toggle-camera": "Activer ou désactiver la caméra",
|
||||
"push-to-talk": "Appuyer pour parler (maintenir pour réactiver)",
|
||||
"reaction": "Panneau des réactions",
|
||||
"fullscreen": "Basculer en plein écran",
|
||||
"recording": "Basculer le panneau d’enregistrement",
|
||||
"raise-hand": "Lever ou baisser la main",
|
||||
"toggle-chat": "Afficher/Masquer le chat",
|
||||
"toggle-participants": "Afficher/Masquer les participants",
|
||||
"open-shortcuts-settings": "Ouvrir les réglages des raccourcis"
|
||||
},
|
||||
"sr": {
|
||||
"control": "Contrôle",
|
||||
"command": "Commande",
|
||||
"alt": "Alt",
|
||||
"option": "Option",
|
||||
"shift": "Majuscule",
|
||||
"plus": "plus",
|
||||
"hold": "Maintenir {{key}}",
|
||||
"noShortcut": "Aucun raccourci"
|
||||
},
|
||||
"visual": {
|
||||
"hold": "Maintenir {{key}}"
|
||||
}
|
||||
},
|
||||
"fullScreenWarning": {
|
||||
"message": "Pour éviter l'affichage en boucle infinie, ne partagez pas l'intégralité de votre écran. Partagez plutôt un onglet ou une autre fenêtre.",
|
||||
"stop": "Arrêter la présentation",
|
||||
|
||||
@@ -100,11 +100,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"listLabel": "Liste des raccourcis clavier",
|
||||
"columnAction": "Action",
|
||||
"columnShortcut": "Raccourci"
|
||||
},
|
||||
"dialog": {
|
||||
"heading": "Paramètres"
|
||||
},
|
||||
@@ -125,7 +120,6 @@
|
||||
"general": "Général",
|
||||
"notifications": "Notifications",
|
||||
"accessibility": "Accessibilité",
|
||||
"transcription": "Transcription",
|
||||
"shortcuts": "Raccourcis"
|
||||
"transcription": "Transcription"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,41 +595,6 @@
|
||||
"muteParticipant": "Demp {{name}}",
|
||||
"fullScreen": "Volledig scherm"
|
||||
},
|
||||
"shortcutsPanel": {
|
||||
"title": "Sneltoetsen",
|
||||
"categories": {
|
||||
"navigation": "Navigatie",
|
||||
"media": "Media",
|
||||
"interaction": "Interactie"
|
||||
},
|
||||
"actions": {
|
||||
"open-shortcuts": "Sneltoetsenhulp openen",
|
||||
"focus-toolbar": "Focus op de onderste werkbalk",
|
||||
"toggle-microphone": "Microfoon aan/uit",
|
||||
"toggle-camera": "Camera aan/uit",
|
||||
"push-to-talk": "Push-to-talk (ingedrukt houden om te activeren)",
|
||||
"reaction": "Reactiepaneel",
|
||||
"fullscreen": "Volledig scherm wisselen",
|
||||
"recording": "Opnamepaneel wisselen",
|
||||
"raise-hand": "Hand opsteken of laten zakken",
|
||||
"toggle-chat": "Chat tonen/verbergen",
|
||||
"toggle-participants": "Deelnemers tonen/verbergen",
|
||||
"open-shortcuts-settings": "Sneltoets-instellingen openen"
|
||||
},
|
||||
"sr": {
|
||||
"control": "Control",
|
||||
"command": "Command",
|
||||
"alt": "Alt",
|
||||
"option": "Option",
|
||||
"shift": "Shift",
|
||||
"plus": "plus",
|
||||
"hold": "Houd {{key}} ingedrukt",
|
||||
"noShortcut": "Geen sneltoets"
|
||||
},
|
||||
"visual": {
|
||||
"hold": "Houd {{key}} ingedrukt"
|
||||
}
|
||||
},
|
||||
"fullScreenWarning": {
|
||||
"message": "Om niet oneindige uw scherm in zichzelf te delen, kunt u beter niet het hele scherm delen. Deel in plaats daarvan een tab of een ander venster.",
|
||||
"stop": "Stop met presenteren",
|
||||
|
||||
@@ -100,11 +100,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"listLabel": "Sneltoetsen",
|
||||
"columnAction": "Actie",
|
||||
"columnShortcut": "Sneltoets"
|
||||
},
|
||||
"dialog": {
|
||||
"heading": "Instellingen"
|
||||
},
|
||||
@@ -119,7 +114,6 @@
|
||||
"video": "Video",
|
||||
"general": "Algemeen",
|
||||
"notifications": "Meldingen",
|
||||
"transcription": "Transcriptie",
|
||||
"shortcuts": "Sneltoetsen"
|
||||
"transcription": "Transcriptie"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,6 @@ const StyledTabs = styled(RACTabs, {
|
||||
flexDirection: 'row',
|
||||
'--vertical': '3px',
|
||||
},
|
||||
'&[data-focus-visible]': {
|
||||
outline: 'none!',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -105,9 +102,6 @@ const StyledTabList = styled(RACTabList, {
|
||||
'&[data-orientation=vertical]': {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
'&[data-focus-visible]': {
|
||||
outline: 'none!',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
border: {
|
||||
@@ -153,8 +147,9 @@ const StyledTabPanel = styled(RACTabPanel, {
|
||||
base: {
|
||||
marginTop: '4px',
|
||||
borderRadius: '4px',
|
||||
outline: 'none',
|
||||
'&[data-focus-visible]': {
|
||||
outline: 'none!',
|
||||
outline: '2px solid red',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
|
||||
@@ -199,11 +199,6 @@ export const buttonRecipe = cva({
|
||||
backgroundColor: 'primaryDark.700',
|
||||
color: 'primaryDark.100',
|
||||
},
|
||||
'&[data-focus-visible]': {
|
||||
outline: '2px solid',
|
||||
outlineColor: 'focusRing',
|
||||
outlineOffset: '2px',
|
||||
},
|
||||
},
|
||||
quaternaryText: {
|
||||
backgroundColor: 'transparent',
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// Map frontend language codes to backend language codes
|
||||
|
||||
export type BackendLanguage = 'en-us' | 'fr-fr' | 'nl-nl' | 'de-de'
|
||||
export type FrontendLanguage = 'en' | 'fr' | 'nl' | 'de'
|
||||
export type BackendLanguage = 'en-us' | 'fr-fr' | 'nl-nl'
|
||||
export type FrontendLanguage = 'en' | 'fr' | 'nl'
|
||||
|
||||
const frontendToBackendMap: Record<FrontendLanguage, BackendLanguage> = {
|
||||
en: 'en-us',
|
||||
fr: 'fr-fr',
|
||||
nl: 'nl-nl',
|
||||
de: 'de-de',
|
||||
}
|
||||
|
||||
export const convertToBackendLanguage = (
|
||||
|
||||
Generated
+366
-778
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.8.0",
|
||||
"version": "1.6.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
"mjml": "4.18.0"
|
||||
"mjml": "4.15.3"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -14,7 +14,7 @@
|
||||
"build": "npm run build-mjml-to-html && npm run build-html-to-plain-text"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.13.1"
|
||||
"node": "22.17.0"
|
||||
},
|
||||
"repository": "https://github.com/suitenumerique/meet",
|
||||
"author": "DINUM",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.8.0",
|
||||
"version": "1.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "1.8.0",
|
||||
"version": "1.6.0",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.8.0",
|
||||
"version": "1.6.0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
FROM python:3.13-alpine3.23 AS base
|
||||
FROM python:3.13-slim AS base
|
||||
|
||||
|
||||
# Install ffmpeg for audio/video processing (format conversion, extraction, compression)
|
||||
# See summary/core/file_service.py for usage.
|
||||
RUN apk add --no-cache "ffmpeg=8.0.1-r1"
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ffmpeg=7:7.1.3-* && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
@@ -11,13 +14,13 @@ WORKDIR /app
|
||||
|
||||
COPY pyproject.toml .
|
||||
|
||||
RUN pip install --no-cache-dir .
|
||||
RUN pip3 install --no-cache-dir .
|
||||
|
||||
FROM base AS development
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN pip install --no-cache-dir -e ".[dev]" || pip install --no-cache-dir -e .
|
||||
RUN pip3 install --no-cache-dir -e ".[dev]" || pip3 install --no-cache-dir -e .
|
||||
|
||||
CMD ["uvicorn", "summary.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
|
||||
@@ -25,15 +28,11 @@ FROM base AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
|
||||
# Remove pip to reduce attack surface in production
|
||||
RUN pip uninstall -y pip
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
COPY ./summary /app/summary
|
||||
|
||||
CMD ["uvicorn", "summary.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "1.8.0"
|
||||
version = "1.6.0"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
Reference in New Issue
Block a user