mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-24 05:22:43 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4759fd97ff | |||
| 1aeb5141b4 | |||
| 75836fc817 | |||
| 1affe65b4a | |||
| c34ecbd3ef | |||
| 78960d9769 | |||
| 41d07d36ee | |||
| f7386e1741 | |||
| f1da04eb27 | |||
| 2970420b84 | |||
| 771f58c0aa | |||
| b159b20695 | |||
| 226d004838 | |||
| b723b7bb62 | |||
| cb36df9104 | |||
| d85123d0c5 | |||
| b2abf814fa | |||
| cfdf1c2f92 | |||
| 4139f542d3 | |||
| eda66640df | |||
| 3fa05ea785 | |||
| 30b68052e1 | |||
| 04fd79b56b | |||
| e336122cfa | |||
| 172dc70649 | |||
| e0ab7f191f | |||
| 455b315dbb | |||
| 3089b03062 | |||
| 60febb3b57 | |||
| bf76ab1ddf |
@@ -252,13 +252,13 @@ jobs:
|
||||
|
||||
- name: Start MinIO
|
||||
run: |
|
||||
docker pull minio/minio
|
||||
docker pull quay.io/minio/minio
|
||||
docker run -d --name minio \
|
||||
-p 9000:9000 \
|
||||
-e "MINIO_ACCESS_KEY=meet" \
|
||||
-e "MINIO_SECRET_KEY=password" \
|
||||
-v /data/media:/data \
|
||||
minio/minio server --console-address :9001 /data
|
||||
quay.io/minio/minio server --console-address :9001 /data
|
||||
|
||||
# Tool to wait for a service to be ready
|
||||
- name: Install Dockerize
|
||||
|
||||
@@ -86,3 +86,6 @@ docker/livekit/rootCA.pem
|
||||
|
||||
# Frontend rollup-plugin-visualizer
|
||||
/src/frontend/rollup-plugin-visualizer/*
|
||||
|
||||
# NixOS
|
||||
.devenv
|
||||
|
||||
@@ -8,6 +8,32 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) make the LiveKit default video codec configurable
|
||||
- 🔧(dev) add support for Bureautix workstations
|
||||
- ✨(frontend) switch frontend images to Caddy and proxy recording/file downloads through it, removing the NGINX Ingress auth annotations dependency
|
||||
|
||||
### Changed
|
||||
|
||||
- 📈(frontend) include LiveKit SIDs in the connection analytics event
|
||||
- 🔇(backend) silence expected 401 warnings on /me
|
||||
- 🔇(backend) silence noisy request summary info logs
|
||||
- ⚡️(frontend) defer loading the Crisp script until idle
|
||||
- ⬆️(frontend) upgrade react-i18next from 17.0.10 to 17.0.12
|
||||
- ⬆️(frontend) upgrade posthog-js from 1.414.0 to 1.418.10
|
||||
- ⬆️(addons) upgrade i18next from 26.3.6 to 26.4.0
|
||||
- ⬆️(frontend) upgrade humanize-duration from 3.33.2 to 3.34.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(backend) report the app release to Sentry instead of "NA"
|
||||
- 🐛(frontend) play the waiting room notification sound on every arrival
|
||||
- 🐛(backend) acknowledge unknown LiveKit webhook events instead of 422
|
||||
- 🔒️(backend) enforce display name setting on rename API
|
||||
- 🔒️(backend) reject inactive users in resource server backend
|
||||
- 🐛(frontend) fix file permissions in the Docker image
|
||||
|
||||
## [1.31.0] - 2026-09-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -292,7 +292,7 @@ shell: ## connect to database shell
|
||||
# -- Database
|
||||
|
||||
dbshell: ## connect to database shell
|
||||
docker compose exec app-dev python manage.py dbshell
|
||||
@$(COMPOSE_EXEC_APP) python manage.py dbshell
|
||||
.PHONY: dbshell
|
||||
|
||||
resetdb: FLUSH_ARGS ?=
|
||||
|
||||
+1
-2
@@ -5,7 +5,7 @@ set -eo pipefail
|
||||
REPO_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd)"
|
||||
UNSET_USER=0
|
||||
|
||||
COMPOSE_FILE="${REPO_DIR}/compose.yml"
|
||||
export COMPOSE_FILE="${COMPOSE_FILE:-${REPO_DIR}/compose.yml}"
|
||||
COMPOSE_PROJECT="meet"
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ function _docker_compose() {
|
||||
echo "🐳(compose) project: '${COMPOSE_PROJECT}' file: '${COMPOSE_FILE}'"
|
||||
docker compose \
|
||||
-p "${COMPOSE_PROJECT}" \
|
||||
-f "${COMPOSE_FILE}" \
|
||||
--project-directory "${REPO_DIR}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# shellcheck source=bin/_config.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
_docker_compose "$@"
|
||||
+2
-2
@@ -17,7 +17,7 @@ services:
|
||||
|
||||
minio:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: minio/minio
|
||||
image: quay.io/minio/minio
|
||||
environment:
|
||||
- MINIO_ROOT_USER=meet
|
||||
- MINIO_ROOT_PASSWORD=password
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
- ./data/media:/data
|
||||
|
||||
createbuckets:
|
||||
image: minio/mc
|
||||
image: quay.io/minio/mc
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1778705847,
|
||||
"narHash": "sha256-EQnZCy7r4VMO6KDoytxHBa0mFbM1D9g1kaDfs/s0YZA=",
|
||||
"ref": "refs/tags/v2.1.2",
|
||||
"rev": "ea3d94ac9d6bf6a1313773170122ca4e2ef5a0be",
|
||||
"revCount": 6569,
|
||||
"type": "git",
|
||||
"url": "https://github.com/cachix/devenv"
|
||||
},
|
||||
"original": {
|
||||
"dir": "src/modules",
|
||||
"ref": "refs/tags/v2.1.2",
|
||||
"type": "git",
|
||||
"url": "https://github.com/cachix/devenv"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1789542786,
|
||||
"narHash": "sha256-ajQuqcxnj6RYnwCjA/9FTCnrdR2+BDzstE8UembXf60=",
|
||||
"ref": "nixos-26.05",
|
||||
"rev": "4c7870105e7f1fdf9c48688c8d7efc21abf0688a",
|
||||
"shallow": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/NixOS/nixpkgs"
|
||||
},
|
||||
"original": {
|
||||
"ref": "nixos-26.05",
|
||||
"shallow": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/NixOS/nixpkgs"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
# =============================================================================
|
||||
# devenv.nix — La Suite Meet ("Visio") developer environment
|
||||
# =============================================================================
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
python = pkgs.python313;
|
||||
nodejs = pkgs.nodejs_22;
|
||||
|
||||
backendDir = "src/backend";
|
||||
agentsDir = "src/agents";
|
||||
summaryDir = "src/summary";
|
||||
frontendDir = "src/frontend";
|
||||
|
||||
readDotEnv =
|
||||
file:
|
||||
let
|
||||
lines = lib.splitString "\n" (builtins.readFile file);
|
||||
unquote =
|
||||
v:
|
||||
let
|
||||
len = builtins.stringLength v;
|
||||
in
|
||||
if len >= 2 && lib.hasPrefix "\"" v && lib.hasSuffix "\"" v then
|
||||
builtins.substring 1 (len - 2) v
|
||||
else if len >= 2 && lib.hasPrefix "'" v && lib.hasSuffix "'" v then
|
||||
builtins.substring 1 (len - 2) v
|
||||
else
|
||||
v;
|
||||
parseLine =
|
||||
line:
|
||||
let
|
||||
m = builtins.match "[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*(.*)" line;
|
||||
in
|
||||
if m == null then null else { name = builtins.elemAt m 0; value = unquote (builtins.elemAt m 1); };
|
||||
in
|
||||
builtins.listToAttrs (builtins.filter (x: x != null) (map parseLine lines));
|
||||
|
||||
# Reuse existing .env
|
||||
dotEnv =
|
||||
(readDotEnv ./env.d/development/common.dist)
|
||||
// (readDotEnv ./env.d/development/postgresql.dist);
|
||||
|
||||
sharedEnv = builtins.removeAttrs dotEnv [ "PYTHONPATH" ]; # only makes sense inside the backend container.
|
||||
in
|
||||
{
|
||||
options.meet = {
|
||||
agents.enable = lib.mkEnableOption "tooling for the LiveKit agents in src/agents";
|
||||
summary.enable = lib.mkEnableOption "tooling for the summary service in src/summary";
|
||||
k8s.enable = lib.mkEnableOption "Kubernetes dev utilities";
|
||||
};
|
||||
|
||||
config = {
|
||||
# Profile can be activated with devenv --profile <profile> shell
|
||||
profiles = {
|
||||
agents.module = {
|
||||
meet.agents.enable = true;
|
||||
};
|
||||
summary.module = {
|
||||
meet.summary.enable = true;
|
||||
};
|
||||
k8s.module = {
|
||||
meet.k8s.enable = true;
|
||||
};
|
||||
};
|
||||
languages.python = {
|
||||
enable = true;
|
||||
package = python;
|
||||
directory = backendDir;
|
||||
manylinux.enable = pkgs.stdenv.hostPlatform.isLinux;
|
||||
|
||||
libraries = [
|
||||
"${config.devenv.dotfile}/profile"
|
||||
pkgs.file
|
||||
pkgs.zlib
|
||||
pkgs.libffi
|
||||
pkgs.openssl
|
||||
];
|
||||
|
||||
uv.enable = true;
|
||||
uv.sync.enable = false;
|
||||
venv.enable = false;
|
||||
lsp.enable = true;
|
||||
};
|
||||
|
||||
languages.javascript = {
|
||||
enable = true;
|
||||
package = nodejs;
|
||||
directory = frontendDir;
|
||||
|
||||
npm.enable = true;
|
||||
yarn.enable = true;
|
||||
corepack.enable = false;
|
||||
};
|
||||
|
||||
languages.typescript.enable = false;
|
||||
languages.nix.enable = true;
|
||||
|
||||
packages =
|
||||
with pkgs;
|
||||
[
|
||||
gnumake
|
||||
file
|
||||
shared-mime-info
|
||||
gettext
|
||||
postgresql_16
|
||||
git
|
||||
curl
|
||||
jq
|
||||
podman
|
||||
podman-compose
|
||||
docker-client
|
||||
]
|
||||
|
||||
# -- LiveKit agents
|
||||
++ lib.optionals config.meet.agents.enable [
|
||||
glib
|
||||
portaudio
|
||||
livekit-cli
|
||||
]
|
||||
|
||||
# -- summary service
|
||||
++ lib.optionals config.meet.summary.enable [
|
||||
redis
|
||||
]
|
||||
|
||||
# -- Kubernetes tools
|
||||
++ lib.optionals config.meet.k8s.enable [
|
||||
kubectl
|
||||
kubernetes-helm
|
||||
helmfile
|
||||
tilt
|
||||
kind
|
||||
mkcert
|
||||
];
|
||||
|
||||
env = sharedEnv // {
|
||||
UV_LINK_MODE = "copy";
|
||||
|
||||
PYTHONDONTWRITEBYTECODE = "1";
|
||||
PYTHONUNBUFFERED = "1";
|
||||
|
||||
UV_PROJECT_ENVIRONMENT = lib.mkForce ".venv";
|
||||
|
||||
COMPOSE_PROJECT_NAME = "meet";
|
||||
|
||||
DJANGO_DATA_DIR = "${config.devenv.root}/data";
|
||||
|
||||
# Database / Pgsql
|
||||
DB_HOST = "127.0.0.1";
|
||||
DB_PORT = "15432";
|
||||
PGHOST = "127.0.0.1";
|
||||
PGPORT = "15432";
|
||||
PGDATABASE = sharedEnv.DB_NAME;
|
||||
PGUSER = sharedEnv.DB_USER;
|
||||
PGPASSWORD = sharedEnv.DB_PASSWORD;
|
||||
|
||||
REDIS_URL = "redis://127.0.0.1:6379/1";
|
||||
CELERY_BROKER_URL = "redis://127.0.0.1:6379/0";
|
||||
|
||||
# S3 / MinIO
|
||||
AWS_S3_ENDPOINT_URL = "http://127.0.0.1:9000";
|
||||
|
||||
# OIDC
|
||||
OIDC_OP_JWKS_ENDPOINT = "http://localhost:8083/realms/meet/protocol/openid-connect/certs";
|
||||
OIDC_OP_TOKEN_ENDPOINT = "http://localhost:8083/realms/meet/protocol/openid-connect/token";
|
||||
OIDC_OP_USER_ENDPOINT = "http://localhost:8083/realms/meet/protocol/openid-connect/userinfo";
|
||||
OIDC_OP_INTROSPECTION_ENDPOINT = "http://localhost:8083/realms/meet/protocol/openid-connect/token/introspect";
|
||||
|
||||
# summary service
|
||||
SUMMARY_SERVICE_ENDPOINT = "http://127.0.0.1:8001/api/v2/async-jobs/transcribe/";
|
||||
SUMMARY_SERVICE_VERSION = "2";
|
||||
|
||||
# Mail
|
||||
DJANGO_EMAIL_HOST = "127.0.0.1";
|
||||
};
|
||||
|
||||
scripts = {
|
||||
|
||||
meet-venv = {
|
||||
description = "Create/refresh meet uv virtualenvs for backend, agents and summary";
|
||||
exec = ''
|
||||
set -euo pipefail
|
||||
cd "$DEVENV_ROOT"
|
||||
|
||||
echo "==> ${backendDir} (uv sync --locked, dependency-groups)"
|
||||
( cd "${backendDir}" && uv sync --locked --all-groups )
|
||||
|
||||
echo "==> ${agentsDir} (uv sync --locked --all-extras)"
|
||||
( cd "${agentsDir}" && uv sync --locked --all-extras )
|
||||
|
||||
echo "==> ${summaryDir} (uv sync --locked --all-extras)"
|
||||
( cd "${summaryDir}" && uv sync --locked --all-extras )
|
||||
|
||||
echo
|
||||
echo "Synced the following virtualenvs successfully:"
|
||||
echo " ${backendDir}/.venv"
|
||||
echo " ${agentsDir}/.venv"
|
||||
echo " ${summaryDir}/.venv"
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
enterShell = ''
|
||||
# Make podman socket accessible in order to launch regular docker commands.
|
||||
# Set MEET_PODMAN_SOCKET=0 to keep the DOCKER_HOST of the calling environment.
|
||||
case "''${MEET_PODMAN_SOCKET:-1}" in
|
||||
0|false|no|off) ;;
|
||||
*)
|
||||
_rundir="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
export DOCKER_HOST="unix://$_rundir/podman/podman.sock"
|
||||
unset _rundir
|
||||
;;
|
||||
esac
|
||||
|
||||
# Compose files to merge
|
||||
_compose_dir="${config.devenv.root}/docker/compose.d"
|
||||
_compose_files="${config.devenv.root}/compose.yml"
|
||||
|
||||
export DOCKER_USER="$(id -u):$(id -g)"
|
||||
|
||||
case "''${DOCKER_HOST:-}" in
|
||||
*podman*)
|
||||
_compose_files="$_compose_files:$_compose_dir/compose.podman.yml"
|
||||
|
||||
# Build images with Podman/Buildah rather than BuildKit. `docker
|
||||
# compose build` otherwise has buildx boot a moby/buildkit container,
|
||||
# and that container lands in its own network namespace with neither
|
||||
# the proxy in its environment nor any route to it.
|
||||
# Buildah has neither problem: base images are resolved by the Podman systemd
|
||||
# service, which inherits the proxy from its systemd socket activated unit, and
|
||||
# RUN steps execute in the *host* network namespace
|
||||
|
||||
export DOCKER_BUILDKIT=0
|
||||
export COMPOSE_BAKE=false
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
# Apply Bureautix override
|
||||
if [ -n "''${http_proxy:-}" ]; then
|
||||
_compose_files="$_compose_files:$_compose_dir/compose.bureautix.yml"
|
||||
fi
|
||||
|
||||
export COMPOSE_FILE="$_compose_files"
|
||||
unset _compose_dir _compose_files
|
||||
|
||||
# Make binaries accessible
|
||||
for _d in \
|
||||
"$DEVENV_ROOT/${backendDir}/.venv/bin" \
|
||||
"$DEVENV_ROOT/${agentsDir}/.venv/bin" \
|
||||
"$DEVENV_ROOT/${summaryDir}/.venv/bin" \
|
||||
"$DEVENV_ROOT/${frontendDir}/node_modules/.bin"
|
||||
do
|
||||
[ -d "$_d" ] && export PATH="$_d:$PATH"
|
||||
done
|
||||
unset _d
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
inputs:
|
||||
nixpkgs:
|
||||
url: git+https://github.com/NixOS/nixpkgs?ref=nixos-26.05&shallow=1
|
||||
devenv:
|
||||
url: git+https://github.com/cachix/devenv?ref=refs/tags/v2.1.2&dir=src/modules
|
||||
@@ -0,0 +1,42 @@
|
||||
# Shared Caddy snippet: proxies recording/file downloads to object storage after
|
||||
# checking authorization with the backend
|
||||
#
|
||||
# Env vars (all optional, fall back to local dev defaults): BACKEND_INTERNAL_HOST,
|
||||
# BACKEND_INTERNAL_PORT, MEDIA_STORAGE_HOST, MEDIA_STORAGE_PROTOCOL, MEDIA_STORAGE_PORT, AWS_STORAGE_BUCKET_NAME.
|
||||
|
||||
handle /media/files/* {
|
||||
route {
|
||||
forward_auth http://{$BACKEND_INTERNAL_HOST:localhost}:{$BACKEND_INTERNAL_PORT:8000} {
|
||||
uri /api/v1.0/files/media-auth/
|
||||
copy_headers Authorization X-Amz-Date X-Amz-Content-Sha256
|
||||
header_up X-Original-URL {http.request.uri}
|
||||
# Backend has SECURE_SSL_REDIRECT: without this the auth subrequest is
|
||||
# 301'd to https, the browser follows it to media-auth (no X-Original-URL) and 403s.
|
||||
header_up X-Forwarded-Proto https
|
||||
}
|
||||
uri replace /media/ /{$AWS_STORAGE_BUCKET_NAME:meet-media-storage}/ 1
|
||||
reverse_proxy {$MEDIA_STORAGE_PROTOCOL:http}://{$MEDIA_STORAGE_HOST:localhost}:{$MEDIA_STORAGE_PORT:9000} {
|
||||
header_up Host {$MEDIA_STORAGE_HOST:localhost}:{$MEDIA_STORAGE_PORT:9000}
|
||||
header_down -Content-Disposition
|
||||
header_down Content-Disposition attachment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Recordings media - kept generic (/media/*) to match the existing ingress path.
|
||||
handle /media/* {
|
||||
route {
|
||||
forward_auth http://{$BACKEND_INTERNAL_HOST:localhost}:{$BACKEND_INTERNAL_PORT:8000} {
|
||||
uri /api/v1.0/recordings/media-auth/
|
||||
copy_headers Authorization X-Amz-Date X-Amz-Content-Sha256
|
||||
header_up X-Original-URL {http.request.uri}
|
||||
header_up X-Forwarded-Proto https
|
||||
}
|
||||
uri replace /media/ /{$AWS_STORAGE_BUCKET_NAME:meet-media-storage}/ 1
|
||||
reverse_proxy {$MEDIA_STORAGE_PROTOCOL:http}://{$MEDIA_STORAGE_HOST:localhost}:{$MEDIA_STORAGE_PORT:9000} {
|
||||
header_up Host {$MEDIA_STORAGE_HOST:localhost}:{$MEDIA_STORAGE_PORT:9000}
|
||||
header_down -Content-Disposition
|
||||
header_down Content-Disposition attachment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Bureautix proxy overrides
|
||||
#
|
||||
# Builds submitted through the Docker API of the Podman service get none of
|
||||
# the proxy variables in their RUN steps. We need to pass the http_proxy explicitely
|
||||
# otherwise all connections fail during the build.
|
||||
|
||||
x-proxy-vars: &proxy-vars
|
||||
http_proxy: ${http_proxy:-}
|
||||
https_proxy: ${https_proxy:-}
|
||||
no_proxy: ${no_proxy:-}
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
app-dev:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
frontend:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
metadata-collector-dev:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
multi-user-transcriber-dev:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
app-summary-dev:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
celery-summary-transcribe:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
celery-summary-summarize:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
# The local proxy listens on 8080 and collides with Keycloak's published admin port.
|
||||
keycloak:
|
||||
ports: !override
|
||||
- "8081:8080"
|
||||
@@ -0,0 +1,33 @@
|
||||
# Rootless Podman override for compose.yml.
|
||||
#
|
||||
# Rootless Podman maps container UID 0 to the host user and every other
|
||||
# container UID to a subuid that owns nothing in the worktree. Meet compose.ymlruns
|
||||
# its containers as DOCKER_USER=$(id -u):$(id -g), which would land on such a
|
||||
# subuid and make every bind mount effectively read-only.
|
||||
#
|
||||
# `userns_mode: keep-id` maps the host user to the same UID and GID inside the
|
||||
# container instead, so DOCKER_USER keeps its Docker value and files written
|
||||
# through a bind mount are owned by the host user on both sides.
|
||||
#
|
||||
# Only the services that mount the worktree and run as DOCKER_USER are listed.
|
||||
|
||||
x-keep-id: &keep-id
|
||||
userns_mode: keep-id
|
||||
|
||||
services:
|
||||
app-dev:
|
||||
<<: *keep-id
|
||||
celery-dev:
|
||||
<<: *keep-id
|
||||
minio:
|
||||
<<: *keep-id
|
||||
node:
|
||||
<<: *keep-id
|
||||
crowdin:
|
||||
<<: *keep-id
|
||||
metadata-collector-dev:
|
||||
<<: *keep-id
|
||||
multi-user-transcriber-dev:
|
||||
<<: *keep-id
|
||||
app-summary-dev:
|
||||
<<: *keep-id
|
||||
@@ -0,0 +1,75 @@
|
||||
# Global options
|
||||
{
|
||||
auto_https off
|
||||
admin off
|
||||
# replace_response can't process compressed bodies, so it must run before encode.
|
||||
order replace before encode
|
||||
}
|
||||
|
||||
:{$PORT} {
|
||||
root * /usr/share/nginx/html
|
||||
encode gzip
|
||||
|
||||
route {
|
||||
import /etc/caddy/media-proxy.caddy
|
||||
|
||||
handle /.well-known/windows-app-web-link {
|
||||
header Content-Type "application/json"
|
||||
header Content-Disposition "attachment; filename=windows-app-web-link"
|
||||
file_server
|
||||
}
|
||||
|
||||
# Manifest — fetched, never iframed
|
||||
handle /addons/outlook/manifest.xml {
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Cache-Control "no-cache, no-store, must-revalidate"
|
||||
header X-Frame-Options "DENY"
|
||||
header Content-Security-Policy "frame-ancestors 'none'"
|
||||
file_server
|
||||
}
|
||||
|
||||
handle /addons/outlook/assets/* {
|
||||
header Cache-Control "public, max-age=2592000, immutable"
|
||||
header Access-Control-Allow-Origin "*"
|
||||
header Vary "Origin"
|
||||
file_server
|
||||
}
|
||||
|
||||
# Outlook add-on pages: per-request CSP nonce, mirrors the Office.js/config.js
|
||||
# script tags baked into the build with a literal NONCE_PLACEHOLDER string.
|
||||
handle /addons/outlook/* {
|
||||
header Cache-Control "no-cache, no-store, must-revalidate"
|
||||
header Pragma "no-cache"
|
||||
header Expires 0
|
||||
header Content-Security-Policy "default-src 'self'; upgrade-insecure-requests; frame-ancestors https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com; script-src 'nonce-{http.request.uuid}' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com; frame-src 'none'; object-src 'none'; base-uri 'none'"
|
||||
replace NONCE_PLACEHOLDER {http.request.uuid}
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
|
||||
# Vite fingerprints everything under /assets, so a given URL's bytes never
|
||||
# change: cache it forever. A new build emits new hashed URLs.
|
||||
@immutable path /assets/*
|
||||
header @immutable Cache-Control "public, max-age=2592000, immutable"
|
||||
|
||||
# The SPA shell and other non-fingerprinted files must revalidate every
|
||||
# load, or a deploy's new asset hashes only show up after a hard refresh.
|
||||
@revalidate not path /assets/*
|
||||
header @revalidate {
|
||||
Cache-Control "no-cache, no-store, must-revalidate"
|
||||
Pragma "no-cache"
|
||||
Expires 0
|
||||
}
|
||||
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
|
||||
handle_errors {
|
||||
@spa_404 expression `{err.status_code} == 404`
|
||||
handle @spa_404 {
|
||||
rewrite * /index.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,19 +52,17 @@ COPY ./src/addons/outlook/ .
|
||||
|
||||
RUN npx webpack --mode production
|
||||
|
||||
# ---- Caddy builder image ----
|
||||
FROM caddy:2.11.4-builder AS caddy-builder
|
||||
|
||||
RUN xcaddy build --with github.com/caddyserver/replace-response
|
||||
RUN apk add --no-cache libcap && \
|
||||
setcap -r /usr/bin/caddy
|
||||
|
||||
# ---- Front-end image ----
|
||||
FROM nginxinc/nginx-unprivileged:1.30.4-alpine3.24 AS frontend-production
|
||||
FROM gcr.io/distroless/static-debian12:nonroot AS frontend-production
|
||||
|
||||
USER root
|
||||
RUN apk del curl
|
||||
USER nginx
|
||||
|
||||
USER nginx
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy
|
||||
|
||||
COPY --from=meet-builder \
|
||||
/home/frontend/dist \
|
||||
@@ -74,9 +72,9 @@ COPY --from=addons-builder \
|
||||
/home/addons/outlook/dist \
|
||||
/usr/share/nginx/html/addons/outlook
|
||||
|
||||
COPY ./docker/dinum-frontend/nginx/default.conf /etc/nginx/conf.d
|
||||
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||
COPY ./docker/dinum-frontend/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY ./docker/caddy/media-proxy.caddy /etc/caddy/media-proxy.caddy
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
|
||||
ENV PORT=8080
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
ENTRYPOINT ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
server_tokens off;
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
# Manifest — fetched, never iframed
|
||||
location = /addons/outlook/manifest.xml {
|
||||
alias /usr/share/nginx/html/addons/outlook/manifest.xml;
|
||||
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header X-Frame-Options "DENY";
|
||||
add_header Content-Security-Policy "frame-ancestors 'none'";
|
||||
}
|
||||
|
||||
location = /addons/outlook/assets/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~* ^/addons/outlook/assets/(.+\.(?:css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot))/?$ {
|
||||
root /usr/share/nginx/html;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000, immutable" always;
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Vary "Origin" always;
|
||||
}
|
||||
|
||||
location = /addons/outlook/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ ^/addons/outlook(/.*)?$ {
|
||||
alias /usr/share/nginx/html/addons/outlook$1;
|
||||
error_page 404 =200 /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires 0 always;
|
||||
|
||||
set $ms_domains "https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com";
|
||||
|
||||
set $nonce $request_id;
|
||||
|
||||
set $csp "default-src 'self'; upgrade-insecure-requests; ";
|
||||
set $csp "${csp}frame-ancestors ${ms_domains}; ";
|
||||
set $csp "${csp}script-src 'nonce-${nonce}' 'strict-dynamic'; ";
|
||||
set $csp "${csp}style-src 'self' 'unsafe-inline'; ";
|
||||
set $csp "${csp}img-src 'self' data:; ";
|
||||
set $csp "${csp}font-src 'self' data:; ";
|
||||
set $csp "${csp}connect-src 'self' ${ms_domains}; ";
|
||||
set $csp "${csp}frame-src 'none'; ";
|
||||
set $csp "${csp}object-src 'none'; ";
|
||||
set $csp "${csp}base-uri 'none'; ";
|
||||
|
||||
add_header Content-Security-Policy $csp;
|
||||
|
||||
sub_filter 'NONCE_PLACEHOLDER' $nonce;
|
||||
sub_filter_once off;
|
||||
}
|
||||
|
||||
location ^~ /assets/mediapipe/wasm/ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
# 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;
|
||||
}
|
||||
@@ -21,3 +21,10 @@ turn:
|
||||
- 192.168.0.0/16
|
||||
- 172.16.0.0/12
|
||||
|
||||
rtc:
|
||||
node_ip: 127.0.0.1
|
||||
advertise_internal_ip: true
|
||||
udp_port: 7882
|
||||
tcp_port: 7881
|
||||
use_external_ip: false
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -10,7 +10,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "3.50.0",
|
||||
"i18next": "26.3.6",
|
||||
"i18next": "26.4.0",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
},
|
||||
@@ -9367,9 +9367,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.3.6",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
|
||||
"integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
|
||||
"version": "26.4.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.0.tgz",
|
||||
"integrity": "sha512-rsmK5bFqsD1AetSFSIa43wtNR4WpvvH4p0tLEsTxkC7QTrfdFm06nbQ95bh8Og4wwaCnUEcm9DVYL2cgxitiQg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "3.50.0",
|
||||
"i18next": "26.3.6",
|
||||
"i18next": "26.4.0",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
FROM python:3.14.6-slim AS base
|
||||
|
||||
# Install system dependencies required by LiveKit
|
||||
RUN apt-get update && apt-get install -y \
|
||||
# Install system dependencies required by LiveKit, fetching packages over HTTPS only for Bureautix proxy
|
||||
RUN sed -i "s|^URIs: http://|URIs: https://|" /etc/apt/sources.list.d/debian.sources \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
libglib2.0-0 \
|
||||
libgobject-2.0-0 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -71,6 +71,7 @@ def get_frontend_configuration(request):
|
||||
"force_wss_protocol": settings.LIVEKIT_FORCE_WSS_PROTOCOL,
|
||||
"enable_firefox_proxy_workaround": settings.LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND,
|
||||
"default_sources": settings.LIVEKIT_DEFAULT_SOURCES,
|
||||
"default_video_codec": settings.LIVEKIT_DEFAULT_VIDEO_CODEC,
|
||||
},
|
||||
"authenticated_users_can_edit_display_name": (
|
||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME
|
||||
|
||||
@@ -909,6 +909,15 @@ class RoomViewSet(
|
||||
"""Rename the current participant in the room."""
|
||||
room = self.get_object()
|
||||
|
||||
if (
|
||||
not settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME
|
||||
and request.user.is_authenticated
|
||||
):
|
||||
return drf_response.Response(
|
||||
{"error": "Authenticated participants cannot edit their display name"},
|
||||
status=drf_status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
serializer = serializers.RenameParticipantSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
|
||||
@@ -286,6 +286,10 @@ class ResourceServerBackend(LaSuiteBackend):
|
||||
if user is None and settings.OIDC_CREATE_USER:
|
||||
user = self.create_user(sub)
|
||||
|
||||
if user is not None and not user.is_active:
|
||||
logger.warning("Inactive user attempted authentication: %s", user.pk)
|
||||
raise SuspiciousOperation("User account is disabled.")
|
||||
|
||||
return user
|
||||
|
||||
def create_user(self, sub):
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Logging filters for the core application."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class SilenceExpected401(logging.Filter):
|
||||
"""Drop the expected 401 from anonymous hits on the /me endpoint.
|
||||
|
||||
The frontend probes `/users/me/` to check authentication; a 401 for
|
||||
anonymous users is normal, not a warning worth logging.
|
||||
"""
|
||||
|
||||
def filter(self, record):
|
||||
"""Return False for a 401 on a silenced path, True otherwise."""
|
||||
if getattr(record, "status_code", None) != 401:
|
||||
return True
|
||||
|
||||
request = getattr(record, "request", None)
|
||||
path = getattr(request, "path", None)
|
||||
if not path:
|
||||
return True
|
||||
|
||||
return path not in settings.LOGGING_SILENCED_401_PATHS
|
||||
@@ -52,12 +52,6 @@ class InvalidPayloadError(LiveKitWebhookError):
|
||||
status_code = 400
|
||||
|
||||
|
||||
class UnsupportedEventTypeError(LiveKitWebhookError):
|
||||
"""Unsupported event type."""
|
||||
|
||||
status_code = 422
|
||||
|
||||
|
||||
class ActionFailedError(LiveKitWebhookError):
|
||||
"""Webhook action fails to process or complete."""
|
||||
|
||||
@@ -74,6 +68,7 @@ class LiveKitWebhookEventType(Enum):
|
||||
# Participant events
|
||||
PARTICIPANT_JOINED = "participant_joined"
|
||||
PARTICIPANT_LEFT = "participant_left"
|
||||
PARTICIPANT_CONNECTION_ABORTED = "participant_connection_aborted"
|
||||
|
||||
# Track events
|
||||
TRACK_PUBLISHED = "track_published"
|
||||
@@ -153,10 +148,13 @@ class LiveKitEventsService:
|
||||
|
||||
try:
|
||||
webhook_type = LiveKitWebhookEventType(data.event)
|
||||
except ValueError as e:
|
||||
raise UnsupportedEventTypeError(
|
||||
f"Unknown webhook type: {data.event}"
|
||||
) from e
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
"Ignoring unknown LiveKit webhook event type '%s' for room '%s'",
|
||||
data.event,
|
||||
room_name,
|
||||
)
|
||||
return
|
||||
|
||||
# Handle according to received webhook type
|
||||
handler = self._webhook_handlers.get(webhook_type.value)
|
||||
|
||||
@@ -372,6 +372,67 @@ def test_rename_participant_unexpected_twirp_error(mock_livekit_client, room, to
|
||||
mock_livekit_client.aclose.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["John Doe", "Admin", "Room Owner"])
|
||||
def test_rename_participant_forbidden_when_display_name_edit_disabled(
|
||||
mock_livekit_client, settings, room, token, name
|
||||
):
|
||||
"""
|
||||
Test rename is rejected for authenticated users when the self-hoster
|
||||
disables AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME.
|
||||
"""
|
||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"name": name}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.data == {
|
||||
"error": "Authenticated participants cannot edit their display name"
|
||||
}
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
|
||||
|
||||
def test_rename_participant_allowed_when_display_name_edit_enabled(
|
||||
mock_livekit_client, settings, room, token
|
||||
):
|
||||
"""Test rename still works for authenticated users when the setting is enabled."""
|
||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = True
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"name": "John Doe"}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
mock_livekit_client.room.update_participant.assert_called_once()
|
||||
|
||||
|
||||
def test_rename_participant_anonymous_allowed_when_display_name_edit_disabled(
|
||||
mock_livekit_client, settings, room, anonymous_token
|
||||
):
|
||||
"""
|
||||
Test the setting only restricts authenticated users: anonymous participants
|
||||
have no account name to fall back on and can still rename themselves.
|
||||
"""
|
||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"name": "Guest User"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {anonymous_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
mock_livekit_client.room.update_participant.assert_called_once()
|
||||
|
||||
|
||||
def test_rename_participant_success_anonymous(
|
||||
mock_livekit_client, room, anonymous_token
|
||||
):
|
||||
|
||||
@@ -117,9 +117,11 @@ def test_start_subtitle_invalid_token():
|
||||
assert response.json() == {"detail": "Invalid LiveKit token: Not enough segments"}
|
||||
|
||||
|
||||
def test_start_subtitle_disabled_by_default(mock_livekit_token):
|
||||
def test_start_subtitle_disabled_by_default(mock_livekit_token, settings):
|
||||
"""Test that subtitle functionality is disabled when feature flag is off."""
|
||||
|
||||
settings.ROOM_SUBTITLE_ENABLED = False
|
||||
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
client = APIClient()
|
||||
|
||||
@@ -94,7 +94,7 @@ def test_invalid_payload(client, auth_token, mock_livekit_config):
|
||||
|
||||
|
||||
def test_unknown_event_type(client, mock_livekit_config):
|
||||
"""Should return 422 for unknown event type."""
|
||||
"""Should acknowledge (200) an unknown event type rather than reject it."""
|
||||
event_data = json.dumps({"event": "unknown_event_type"})
|
||||
|
||||
# Generate auth token for this specific payload
|
||||
@@ -112,10 +112,8 @@ def test_unknown_event_type(client, mock_livekit_config):
|
||||
HTTP_AUTHORIZATION=auth_token,
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"status": "error",
|
||||
}
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "success"}
|
||||
|
||||
|
||||
@mock.patch.object(LiveKitEventsService, "_handle_room_finished")
|
||||
|
||||
@@ -16,7 +16,6 @@ from core.services.livekit_events import (
|
||||
AuthenticationError,
|
||||
InvalidPayloadError,
|
||||
LiveKitEventsService,
|
||||
UnsupportedEventTypeError,
|
||||
api,
|
||||
)
|
||||
from core.services.lobby import LobbyService
|
||||
@@ -665,22 +664,27 @@ def test_receive_missing_auth(service):
|
||||
|
||||
|
||||
@mock.patch.object(api.WebhookReceiver, "receive")
|
||||
def test_receive_unsupported_event(mock_receive, service):
|
||||
"""Should raise LiveKitWebhookError for unsupported events."""
|
||||
def test_receive_unknown_event_is_acknowledged(mock_receive, service, caplog):
|
||||
"""Unknown event types are logged and ignored, not rejected.
|
||||
|
||||
LiveKit adds event types over time and does not retry 4xx responses, so
|
||||
raising here would silently drop the event.
|
||||
"""
|
||||
mock_request = mock.MagicMock()
|
||||
mock_request.headers = {"Authorization": "test_token"}
|
||||
mock_request.body = b"{}"
|
||||
|
||||
# Mock returned data with unsupported event type
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.room.name = str(uuid.uuid4())
|
||||
mock_data.event = "unsupported_event"
|
||||
mock_data.event = "some_future_event"
|
||||
mock_receive.return_value = mock_data
|
||||
|
||||
with pytest.raises(
|
||||
UnsupportedEventTypeError, match="Unknown webhook type: unsupported_event"
|
||||
):
|
||||
service.receive(mock_request)
|
||||
with caplog.at_level("WARNING", logger="core.services.livekit_events"):
|
||||
service.receive(mock_request) # must not raise
|
||||
|
||||
assert "Ignoring unknown LiveKit webhook event type 'some_future_event'" in (
|
||||
caplog.text
|
||||
)
|
||||
|
||||
|
||||
@mock.patch.object(api.WebhookReceiver, "receive")
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Tests for the external API ResourceServerBackend."""
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core.external_api.authentication import ResourceServerBackend
|
||||
from core.factories import UserFactory
|
||||
from core.models import User
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def _payload(sub):
|
||||
return {"sub": sub, "active": True, "scope": "lasuite_meet", "client_id": "app"}
|
||||
|
||||
|
||||
def test_resource_server_backend_get_or_create_user_active():
|
||||
"""An existing active user matching the sub should be returned."""
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
result = ResourceServerBackend().get_or_create_user(
|
||||
access_token="token", id_token=None, payload=_payload(user.sub)
|
||||
)
|
||||
|
||||
assert result == user
|
||||
|
||||
|
||||
def test_resource_server_backend_get_or_create_user_inactive():
|
||||
"""An inactive user should be rejected even with a valid token."""
|
||||
|
||||
user = UserFactory(is_active=False)
|
||||
|
||||
with pytest.raises(SuspiciousOperation, match="User account is disabled."):
|
||||
ResourceServerBackend().get_or_create_user(
|
||||
access_token="token", id_token=None, payload=_payload(user.sub)
|
||||
)
|
||||
|
||||
|
||||
def test_resource_server_backend_get_or_create_user_creates(settings):
|
||||
"""An unknown sub should create an active user when OIDC_CREATE_USER is set."""
|
||||
|
||||
settings.OIDC_CREATE_USER = True
|
||||
|
||||
result = ResourceServerBackend().get_or_create_user(
|
||||
access_token="token", id_token=None, payload=_payload("new-sub")
|
||||
)
|
||||
|
||||
assert result.sub == "new-sub"
|
||||
assert result.is_active is True
|
||||
assert User.objects.filter(sub="new-sub").exists()
|
||||
|
||||
|
||||
def test_resource_server_backend_get_or_create_user_no_creation(settings):
|
||||
"""An unknown sub should return None when OIDC_CREATE_USER is unset."""
|
||||
|
||||
settings.OIDC_CREATE_USER = False
|
||||
|
||||
result = ResourceServerBackend().get_or_create_user(
|
||||
access_token="token", id_token=None, payload=_payload("new-sub")
|
||||
)
|
||||
|
||||
assert result is None
|
||||
assert not User.objects.filter(sub="new-sub").exists()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_rooms_list_resource_server_inactive_user(settings):
|
||||
"""End to end: a valid introspected token for an inactive user should get 401."""
|
||||
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
|
||||
user = UserFactory(is_active=False)
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={
|
||||
"iss": "https://oidc.example.com",
|
||||
"active": True,
|
||||
"sub": user.sub,
|
||||
"scope": "openid lasuite_meet rooms:list",
|
||||
"client_id": "app",
|
||||
},
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer rs-token")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "login failed" in str(response.data).lower()
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Unit tests for the get_release settings helper."""
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from meet.settings import get_release
|
||||
|
||||
|
||||
@pytest.fixture(name="base_dir")
|
||||
def fixture_empty_base_dir(tmp_path, monkeypatch):
|
||||
"""Point get_release at an empty directory."""
|
||||
monkeypatch.setattr("meet.settings.BASE_DIR", str(tmp_path))
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_get_release_reads_project_pyproject():
|
||||
"""Should return the semantic version of the backend's pyproject.toml."""
|
||||
assert re.fullmatch(r"\d+\.\d+\.\d+", get_release())
|
||||
|
||||
|
||||
def test_get_release_reads_pyproject_version(base_dir):
|
||||
"""Should return the version declared in the [project] table."""
|
||||
(base_dir / "pyproject.toml").write_text(
|
||||
'[project]\nname = "meet"\nversion = "1.2.3"\n', encoding="utf-8"
|
||||
)
|
||||
assert get_release() == "1.2.3"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("base_dir")
|
||||
def test_get_release_missing_pyproject():
|
||||
"""Should fall back to "NA" without a pyproject.toml."""
|
||||
assert get_release() == "NA"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"content",
|
||||
[
|
||||
'[project]\nname = "meet"\n', # no version
|
||||
"[tool.uv]\npackage = true\n", # no [project] table
|
||||
"[project\nversion = ", # malformed TOML
|
||||
],
|
||||
)
|
||||
def test_get_release_unreadable_version(base_dir, content):
|
||||
"""Should fall back to "NA" without a readable version in pyproject.toml."""
|
||||
(base_dir / "pyproject.toml").write_text(content, encoding="utf-8")
|
||||
assert get_release() == "NA"
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Unit tests for the LIVEKIT_DEFAULT_VIDEO_CODEC setting value."""
|
||||
|
||||
import pytest
|
||||
|
||||
from meet.settings import VideoCodecValue
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw,expected",
|
||||
[("vp9", "vp9"), ("AV1", "av1"), (" h264 ", "h264")],
|
||||
)
|
||||
def test_video_codec_value_normalizes(raw, expected):
|
||||
"""Whitespace is trimmed and the name is lowercased before it is checked."""
|
||||
# environ=False keeps __new__ from resolving the value, so the instance survives.
|
||||
assert VideoCodecValue(environ=False).to_python(raw) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", ["vp10", "", "h.264"])
|
||||
def test_video_codec_value_rejects_unsupported(raw):
|
||||
"""A name outside the accepted list stops the settings module loading."""
|
||||
with pytest.raises(ValueError, match="Unsupported video codec"):
|
||||
VideoCodecValue(environ=False).to_python(raw)
|
||||
@@ -12,7 +12,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import json
|
||||
import tomllib
|
||||
import warnings
|
||||
from os import path
|
||||
from socket import gethostbyname, gethostname
|
||||
@@ -37,22 +37,34 @@ GB = 1024 * MB
|
||||
def get_release():
|
||||
"""
|
||||
Get the current release of the application
|
||||
|
||||
By release, we mean the release from the version.json file à la Mozilla [1]
|
||||
(if any). If this file has not been found, it defaults to "NA".
|
||||
|
||||
[1]
|
||||
https://github.com/mozilla-services/Dockerflow/blob/master/docs/version_object.md
|
||||
"""
|
||||
# Try to get the current release from the version.json file generated by the
|
||||
# CI during the Docker image build
|
||||
try:
|
||||
with open(path.join(BASE_DIR, "version.json"), encoding="utf8") as version:
|
||||
return json.load(version)["version"]
|
||||
except FileNotFoundError:
|
||||
with open(path.join(BASE_DIR, "pyproject.toml"), "rb") as pyproject:
|
||||
return tomllib.load(pyproject)["project"]["version"]
|
||||
except (FileNotFoundError, KeyError, tomllib.TOMLDecodeError):
|
||||
return "NA" # Default: not available
|
||||
|
||||
|
||||
class VideoCodecValue(values.Value):
|
||||
"""
|
||||
A video codec name, normalized to lowercase and validated against the codecs
|
||||
supported by the LiveKit client, so that a typo fails at startup instead of
|
||||
silently downgrading every publisher to another codec.
|
||||
"""
|
||||
|
||||
codecs = frozenset(("vp8", "h264", "vp9", "av1"))
|
||||
|
||||
def to_python(self, value):
|
||||
"""Normalize the codec name and ensure it is a supported one."""
|
||||
codec = super().to_python(value).strip().lower()
|
||||
if codec not in self.codecs:
|
||||
raise ValueError(
|
||||
f"Unsupported video codec {value!r}, "
|
||||
f"expected one of: {', '.join(sorted(self.codecs))}."
|
||||
)
|
||||
return codec
|
||||
|
||||
|
||||
class Base(Configuration):
|
||||
"""
|
||||
This is the base configuration every configuration (aka environment) should inherit from. It
|
||||
@@ -469,6 +481,9 @@ class Base(Configuration):
|
||||
|
||||
# Sentry
|
||||
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN")
|
||||
SENTRY_TRACES_SAMPLE_RATE = values.FloatValue(
|
||||
0.0, environ_name="SENTRY_TRACES_SAMPLE_RATE", environ_prefix=None
|
||||
)
|
||||
|
||||
# Easy thumbnails
|
||||
THUMBNAIL_EXTENSION = "webp"
|
||||
@@ -675,6 +690,9 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
default=False,
|
||||
)
|
||||
LIVEKIT_DEFAULT_VIDEO_CODEC = VideoCodecValue(
|
||||
"vp9", environ_name="LIVEKIT_DEFAULT_VIDEO_CODEC", environ_prefix=None
|
||||
)
|
||||
CONNECTION_TEST_ENABLED = values.BooleanValue(
|
||||
environ_name="CONNECTION_TEST_ENABLED",
|
||||
environ_prefix=None,
|
||||
@@ -1110,6 +1128,12 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
LOGGING_SILENCED_401_PATHS = values.ListValue(
|
||||
default=["/api/v1.0/users/me/"],
|
||||
environ_name="LOGGING_SILENCED_401_PATHS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Logging
|
||||
# We want to make it easy to log to console but by default we log production
|
||||
# to Sentry and don't want to log to console.
|
||||
@@ -1122,10 +1146,16 @@ class Base(Configuration):
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"filters": {
|
||||
"silence_expected_401": {
|
||||
"()": "core.logging_filters.SilenceExpected401",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "simple",
|
||||
"filters": ["silence_expected_401"],
|
||||
},
|
||||
},
|
||||
# Override root logger to send it to console
|
||||
@@ -1136,6 +1166,13 @@ class Base(Configuration):
|
||||
),
|
||||
},
|
||||
"loggers": {
|
||||
"request.summary": {
|
||||
"level": values.Value(
|
||||
"WARNING",
|
||||
environ_name="LOGGING_LEVEL_REQUEST_SUMMARY",
|
||||
environ_prefix="",
|
||||
)
|
||||
},
|
||||
"core": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
@@ -1211,7 +1248,14 @@ class Base(Configuration):
|
||||
dsn=cls.SENTRY_DSN,
|
||||
environment=cls.__name__.lower(), # build, test, development, production
|
||||
release=get_release(),
|
||||
integrations=[DjangoIntegration()],
|
||||
traces_sample_rate=cls.SENTRY_TRACES_SAMPLE_RATE,
|
||||
integrations=[
|
||||
DjangoIntegration(
|
||||
transaction_style="url",
|
||||
middleware_spans=True,
|
||||
cache_spans=True,
|
||||
)
|
||||
],
|
||||
)
|
||||
sentry_sdk.set_tag("application", "backend")
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Global options
|
||||
{
|
||||
auto_https off
|
||||
admin off
|
||||
}
|
||||
|
||||
:{$PORT} {
|
||||
root * /usr/share/nginx/html
|
||||
encode gzip
|
||||
|
||||
# Vite fingerprints everything under /assets, so a given URL's bytes never
|
||||
# change: cache it forever. A new build emits new hashed URLs.
|
||||
@immutable path /assets/*
|
||||
header @immutable Cache-Control "public, max-age=2592000, immutable"
|
||||
|
||||
# The SPA shell and other non-fingerprinted files must revalidate every
|
||||
# load, or a deploy's new asset hashes only show up after a hard refresh.
|
||||
@revalidate not path /assets/*
|
||||
header @revalidate {
|
||||
Cache-Control "no-cache, no-store, must-revalidate"
|
||||
Pragma "no-cache"
|
||||
Expires 0
|
||||
}
|
||||
|
||||
route {
|
||||
import /etc/caddy/media-proxy.caddy
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
|
||||
handle_errors {
|
||||
@spa_404 expression `{err.status_code} == 404`
|
||||
handle @spa_404 {
|
||||
rewrite * /index.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-16
@@ -4,12 +4,12 @@ USER node
|
||||
|
||||
WORKDIR /home/frontend/
|
||||
|
||||
COPY ./src/frontend/package.json ./package.json
|
||||
COPY ./src/frontend/package-lock.json ./package-lock.json
|
||||
COPY --chown=node:node ./src/frontend/package.json ./package.json
|
||||
COPY --chown=node:node ./src/frontend/package-lock.json ./package-lock.json
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY .dockerignore ./.dockerignore
|
||||
COPY --chown=node:node .dockerignore ./.dockerignore
|
||||
COPY --chown=node:node ./src/frontend/ .
|
||||
|
||||
### ---- Front-end builder image ----
|
||||
@@ -41,24 +41,24 @@ ENV VITE_APP_TITLE=${VITE_APP_TITLE}
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# ---- Caddy builder image ----
|
||||
FROM caddy:2.11.4-builder AS caddy-builder
|
||||
|
||||
RUN xcaddy build --with github.com/caddyserver/replace-response
|
||||
RUN apk add --no-cache libcap && \
|
||||
setcap -r /usr/bin/caddy
|
||||
|
||||
# ---- Front-end image ----
|
||||
FROM nginxinc/nginx-unprivileged:1.30.4-alpine3.24 AS frontend-production
|
||||
|
||||
USER root
|
||||
RUN apk del curl
|
||||
USER nginx
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
FROM gcr.io/distroless/static-debian12:nonroot AS frontend-production
|
||||
|
||||
COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy
|
||||
COPY --from=meet-builder \
|
||||
/home/frontend/dist \
|
||||
/usr/share/nginx/html
|
||||
|
||||
COPY ./src/frontend/default.conf /etc/nginx/conf.d
|
||||
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||
COPY ./src/frontend/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY ./docker/caddy/media-proxy.caddy /etc/caddy/media-proxy.caddy
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
|
||||
ENV PORT=8080
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
ENTRYPOINT ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
server_tokens off;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location ^~ /assets/mediapipe/wasm/ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
# 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;
|
||||
}
|
||||
Generated
+36
-43
@@ -23,18 +23,18 @@
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.1.2",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.2",
|
||||
"humanize-duration": "3.34.1",
|
||||
"i18next": "26.3.6",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"i18next-parser": "9.4.0",
|
||||
"i18next-resources-to-backend": "1.2.3",
|
||||
"livekit-client": "2.21.0",
|
||||
"posthog-js": "1.414.0",
|
||||
"posthog-js": "1.418.10",
|
||||
"react": "18.3.1",
|
||||
"react-aria": "3.50.0",
|
||||
"react-aria-components": "1.19.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "17.0.10",
|
||||
"react-i18next": "17.0.12",
|
||||
"react-stately": "3.48.0",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.3.2",
|
||||
@@ -1720,28 +1720,28 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@posthog/browser-common": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.4.0.tgz",
|
||||
"integrity": "sha512-W9DCGVks15docUMPvJ2nd8NS16Gn74bsGWuaeg31beEKFSjdW8wvnQ1ETY6WSql5pYxZb3GdJmEUZVVstKSrBQ==",
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.5.2.tgz",
|
||||
"integrity": "sha512-8GvfEshFdeKIccuy3kpp6mDBxawQtRamMYRCwzy1r1ixLKQVLAGs3afhOf6r75yp59ZQBi5mtXQgUJ2Jz8eHuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@posthog/core": "^1.46.8",
|
||||
"@posthog/types": "^1.402.0"
|
||||
"@posthog/core": "^1.48.11",
|
||||
"@posthog/types": "^1.405.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.49.1.tgz",
|
||||
"integrity": "sha512-jdZh85tG56OXLH881CVwBZyiXCPPaZasfYeWwm9kVUvxC/Rb+lz7wYN9GuqSEmNPJgVnK4v8wS0bCaFc3OmVEA==",
|
||||
"version": "1.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.53.2.tgz",
|
||||
"integrity": "sha512-Knx8442G2LyPVIzLvhcBX/TIdeHpZhrYrtZou3Uw8FWfbr9gQtQeOZDCog3MWcDxJ+4vAnMAAOzf5cYpc7Gxyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@posthog/types": "^1.407.0"
|
||||
"@posthog/types": "^1.411.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/types": {
|
||||
"version": "1.407.1",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.407.1.tgz",
|
||||
"integrity": "sha512-WhbkXPC2rgylXqmxHqv70ffI3k+KxyR6s7DBIfr5NvIqHkxp6v0pk31D/jbz0DNVbzwkLjyll2pxr4FNbJiYzg==",
|
||||
"version": "1.411.1",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.411.1.tgz",
|
||||
"integrity": "sha512-Gd7tnSYctcSXup3naVlAgavvenByRao6rsSYdMgWgP35KE9jFn+rMHNRJeI5LNjJuLzMXHEVcgzAN7xcATefdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-aria/overlays": {
|
||||
@@ -6354,11 +6354,12 @@
|
||||
"integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz",
|
||||
"integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://locize.com"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
@@ -6401,9 +6402,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-duration": {
|
||||
"version": "3.33.2",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.2.tgz",
|
||||
"integrity": "sha512-K7Ny/ULO1hDm2nnhvAY+SJV1skxFb61fd073SG1IWJl+D44ULrruCuTyjHKjBVVcSuTlnY99DKtgEG39CM5QOQ==",
|
||||
"version": "3.34.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.34.1.tgz",
|
||||
"integrity": "sha512-YIiigjQ+O31rvcyDJPK1ptTZtvSErjBtmHy93VhbwxB/VwMG0FszKs2IB667tFxmOhNapePQQxa24luQRDkKFQ==",
|
||||
"license": "Unlicense",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/EvanHahn"
|
||||
@@ -9131,16 +9132,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.414.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.414.0.tgz",
|
||||
"integrity": "sha512-dtZd4asdskr8lNyltAEX6zyn48uO1pO0EMvx6AXJU65PFhu6yn2LPbKtQcyLysjcN57FJPNT9QYL6St5SBJHqw==",
|
||||
"version": "1.418.10",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.418.10.tgz",
|
||||
"integrity": "sha512-XMvmmnuFoesSPjFo+wvzXkvuzYqIho8CVqxugG1Wt768offFARYNZDd1/xWfm9vk/pJTJ4RhUEHRzLpggmGx9Q==",
|
||||
"license": "(Apache-2.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@posthog/browser-common": "^0.4.0",
|
||||
"@posthog/core": "^1.46.9",
|
||||
"@posthog/types": "^1.402.2",
|
||||
"@posthog/browser-common": "^0.5.0",
|
||||
"@posthog/core": "^1.48.8",
|
||||
"@posthog/types": "^1.405.1",
|
||||
"core-js": "^3.49.0",
|
||||
"dompurify": "^3.4.12",
|
||||
"dompurify": "^3.4.13",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.29.3",
|
||||
"query-selector-shadow-dom": "^1.0.1",
|
||||
@@ -9435,13 +9436,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "17.0.10",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.10.tgz",
|
||||
"integrity": "sha512-XneHftyYA774MJkkccSkZ5oKrUpCnXIPmxio3wemqrVzCRLWiGXOMbIzObrer03fNDEnm8g8R5yYls4HcE+esg==",
|
||||
"version": "17.0.12",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.12.tgz",
|
||||
"integrity": "sha512-lFWPEGkxQ6RhusdUkysFBD58VHfSSzvHBzqMgN0SvfVpdQGfwtNkStTqdy08/sJd7s807qqutgx93fRpD0DJ3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"@babel/runtime": "^7.29.7",
|
||||
"html-parse-stringify": "^4.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -11608,14 +11609,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/walk-sync": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz",
|
||||
|
||||
@@ -30,18 +30,18 @@
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.1.2",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.2",
|
||||
"humanize-duration": "3.34.1",
|
||||
"i18next": "26.3.6",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"i18next-parser": "9.4.0",
|
||||
"i18next-resources-to-backend": "1.2.3",
|
||||
"livekit-client": "2.21.0",
|
||||
"posthog-js": "1.414.0",
|
||||
"posthog-js": "1.418.10",
|
||||
"react": "18.3.1",
|
||||
"react-aria": "3.50.0",
|
||||
"react-aria-components": "1.19.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "17.0.10",
|
||||
"react-i18next": "17.0.12",
|
||||
"react-stately": "3.48.0",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.3.2",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { keys } from './queryKeys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { RecordingMode } from '@/features/recording'
|
||||
import type { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
|
||||
import type { Track } from 'livekit-client'
|
||||
import type { Track, VideoCodec } from 'livekit-client'
|
||||
type Source = Track.Source
|
||||
|
||||
export interface ApiConfig {
|
||||
@@ -62,6 +62,7 @@ export interface ApiConfig {
|
||||
force_wss_protocol: boolean
|
||||
enable_firefox_proxy_workaround: boolean
|
||||
default_sources: Source[]
|
||||
default_video_codec: VideoCodec
|
||||
}
|
||||
transcription_destination?: string
|
||||
max_participants_for_sound: number
|
||||
|
||||
+9
-2
@@ -34,6 +34,15 @@ export const WaitingParticipantNotification = () => {
|
||||
const isParticipantListEmpty = (p?: WaitingParticipant[]) => p?.length == 0
|
||||
|
||||
useEffect(() => {
|
||||
const previousIds = new Set(prevWaitingParticipant?.map(({ id }) => id))
|
||||
const hasNewWaitingParticipant = waitingParticipants.some(
|
||||
({ id }) => !previousIds.has(id)
|
||||
)
|
||||
|
||||
if (hasNewWaitingParticipant) {
|
||||
triggerNotificationSound(NotificationType.ParticipantWaiting)
|
||||
}
|
||||
|
||||
// Show notification when the first participant enters the waiting room
|
||||
if (
|
||||
!isParticipantListEmpty(waitingParticipants) &&
|
||||
@@ -42,8 +51,6 @@ export const WaitingParticipantNotification = () => {
|
||||
) {
|
||||
setShowQuickActionsMessage(true)
|
||||
|
||||
triggerNotificationSound(NotificationType.ParticipantJoined)
|
||||
|
||||
if (timerRef.current !== null) {
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const useNotificationSound = () => {
|
||||
participantJoined: [0, 1150],
|
||||
handRaised: [1400, 180],
|
||||
messageReceived: [1580, 300],
|
||||
waiting: [2039, 710],
|
||||
participantWaiting: [2039, 710],
|
||||
success: [2740, 1304],
|
||||
},
|
||||
volume: notificationsSnap.soundNotificationVolume,
|
||||
|
||||
@@ -107,7 +107,7 @@ export const Conference = ({
|
||||
adaptiveStream: true,
|
||||
dynacast: true,
|
||||
publishDefaults: {
|
||||
videoCodec: 'vp9',
|
||||
videoCodec: apiConfig?.livekit.default_video_codec ?? 'vp9',
|
||||
},
|
||||
videoCaptureDefaults: {
|
||||
deviceId: userConfig.videoDeviceId ?? undefined,
|
||||
@@ -129,6 +129,7 @@ export const Conference = ({
|
||||
userConfig.videoPublishResolution,
|
||||
userConfig.audioDeviceId,
|
||||
userConfig.audioOutputDeviceId,
|
||||
apiConfig?.livekit.default_video_codec,
|
||||
])
|
||||
|
||||
const room = useMemo(() => new Room(roomOptions), [roomOptions])
|
||||
|
||||
@@ -71,20 +71,30 @@ export const ConnectionObserver = () => {
|
||||
useEffect(() => {
|
||||
if (!isAnalyticsEnabled) return
|
||||
|
||||
const handleConnection = () => {
|
||||
const handleConnection = async () => {
|
||||
// Preserve original connection timestamp across reconnections to measure
|
||||
// total session duration from first connect to final disconnect.
|
||||
if (connectionStartTimeRef.current != null) return
|
||||
connectionStartTimeRef.current = Date.now()
|
||||
void captureMediaEvent('connection-event', {})
|
||||
const participantSid = room.localParticipant.sid
|
||||
const roomSid = await room.getSid().catch(() => undefined)
|
||||
void captureMediaEvent('connection-event', {
|
||||
livekit_room_sid: roomSid,
|
||||
livekit_participant_sid: participantSid,
|
||||
})
|
||||
}
|
||||
|
||||
const handleReconnect = () => {
|
||||
captureEvent('reconnect-event')
|
||||
}
|
||||
|
||||
const handleReconnected = () => {
|
||||
captureEvent('reconnected-event')
|
||||
const handleReconnected = async () => {
|
||||
const participantSid = room.localParticipant.sid
|
||||
const roomSid = await room.getSid().catch(() => undefined)
|
||||
captureEvent('reconnected-event', {
|
||||
livekit_room_sid: roomSid,
|
||||
livekit_participant_sid: participantSid,
|
||||
})
|
||||
}
|
||||
|
||||
const handleSignalingConnect = () => {
|
||||
|
||||
+6
-6
@@ -2,23 +2,23 @@ import { RiQuestionLine } from '@remixicon/react'
|
||||
import { MenuItem } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||
import { Crisp } from 'crisp-sdk-web'
|
||||
import { useIsSupportEnabled } from '@/features/support/hooks/useSupport'
|
||||
import {
|
||||
useIsSupportEnabled,
|
||||
openSupportChat,
|
||||
} from '@/features/support/hooks/useSupport'
|
||||
|
||||
export const SupportMenuItem = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||
const isSupportEnabled = useIsSupportEnabled()
|
||||
|
||||
if (!isSupportEnabled || !Crisp) {
|
||||
if (!isSupportEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
||||
onAction={() => {
|
||||
Crisp?.chat.open()
|
||||
}}
|
||||
onAction={openSupportChat}
|
||||
>
|
||||
<RiQuestionLine size={20} />
|
||||
{t('support')}
|
||||
|
||||
@@ -1,20 +1,45 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Crisp } from 'crisp-sdk-web'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { type ApiUser } from '@/features/auth/api/ApiUser'
|
||||
import { useUser } from '@/features/auth/api/useUser'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
|
||||
type CrispSdk = (typeof import('crisp-sdk-web'))['Crisp']
|
||||
|
||||
let crisp: CrispSdk | undefined
|
||||
let crispPromise: Promise<CrispSdk> | undefined
|
||||
|
||||
const loadCrisp = (): Promise<CrispSdk> => {
|
||||
crispPromise ??= import('crisp-sdk-web')
|
||||
.then((module) => {
|
||||
crisp = module.Crisp
|
||||
return module.Crisp
|
||||
})
|
||||
.catch((error) => {
|
||||
crispPromise = undefined
|
||||
throw error
|
||||
})
|
||||
|
||||
return crispPromise
|
||||
}
|
||||
|
||||
export const openSupportChat = () => {
|
||||
if (!crisp?.isCrispInjected()) return
|
||||
crisp.chat.open()
|
||||
}
|
||||
|
||||
export const initializeSupportSession = (user: ApiUser) => {
|
||||
if (!Crisp.isCrispInjected()) return
|
||||
if (!crisp?.isCrispInjected()) return
|
||||
|
||||
const { id, email } = user
|
||||
Crisp.setTokenId(`meet-${id}`)
|
||||
if (email) Crisp.user.setEmail(email)
|
||||
crisp.setTokenId(`meet-${id}`)
|
||||
if (email) crisp.user.setEmail(email)
|
||||
}
|
||||
|
||||
export const terminateSupportSession = () => {
|
||||
if (!Crisp.isCrispInjected()) return
|
||||
Crisp.setTokenId()
|
||||
Crisp.session.reset()
|
||||
if (!crisp?.isCrispInjected()) return
|
||||
|
||||
crisp.setTokenId()
|
||||
crisp.session.reset()
|
||||
}
|
||||
|
||||
export type useSupportProps = {
|
||||
@@ -22,26 +47,70 @@ export type useSupportProps = {
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
// Configure Crisp chat for real-time support across all pages.
|
||||
const IDLE_TIMEOUT_MS = 10_000
|
||||
|
||||
const scheduleWhenIdle = (callback: () => void): (() => void) => {
|
||||
if (typeof window.requestIdleCallback === 'function') {
|
||||
const handle = window.requestIdleCallback(callback, {
|
||||
timeout: IDLE_TIMEOUT_MS,
|
||||
})
|
||||
return () => window.cancelIdleCallback(handle)
|
||||
}
|
||||
|
||||
const handle = window.setTimeout(callback, 1)
|
||||
return () => window.clearTimeout(handle)
|
||||
}
|
||||
|
||||
export const useSupport = ({ id, isDisabled }: useSupportProps) => {
|
||||
const { user } = useUser()
|
||||
const [isInjected, setIsInjected] = useState(
|
||||
() => crisp?.isCrispInjected() ?? false
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || Crisp.isCrispInjected() || isDisabled) return
|
||||
Crisp.configure(id)
|
||||
Crisp.setHideOnMobile(true)
|
||||
if (!id || isDisabled) return
|
||||
|
||||
if (crisp?.isCrispInjected()) {
|
||||
setIsInjected(true)
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const cancelIdle = scheduleWhenIdle(() => {
|
||||
void loadCrisp()
|
||||
.then((sdk) => {
|
||||
if (cancelled) return
|
||||
|
||||
if (!sdk.isCrispInjected()) {
|
||||
sdk.configure(id)
|
||||
sdk.setHideOnMobile(true)
|
||||
}
|
||||
|
||||
setIsInjected(true)
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) {
|
||||
console.error('Failed to initialize support chat', error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
cancelIdle()
|
||||
}
|
||||
}, [id, isDisabled])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return
|
||||
if (!user || !isInjected || isDisabled) return
|
||||
initializeSupportSession(user)
|
||||
}, [user])
|
||||
}, [user, isInjected, isDisabled])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Some users may block Crisp chat widget with browser ad blockers or anti-tracking plugins
|
||||
// So we need to safely check if Crisp is available and not blocked
|
||||
// Some users block the chat widget, so check its availability safely.
|
||||
const isCrispAvailable = () => {
|
||||
try {
|
||||
return !!window?.$crisp?.is
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "Ein neues Dokument wird erstellt auf",
|
||||
"destinationUnknown": "Ein neues Dokument wird erstellt",
|
||||
"language": "Meeting-Sprache:",
|
||||
"recording": "Auch eine Aufzeichnung starten"
|
||||
"recording": "Auch eine Videoaufzeichnung starten"
|
||||
},
|
||||
"button": {
|
||||
"start": "Meeting-Transkription starten",
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"messageReceived": {
|
||||
"label": "Nachricht erhalten"
|
||||
},
|
||||
"participantWaiting": {
|
||||
"label": "Person im Warteraum"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "A new document will be created on",
|
||||
"destinationUnknown": "A new document will be created",
|
||||
"language": "Meeting language:",
|
||||
"recording": "Also start a recording"
|
||||
"recording": "Also start a video recording"
|
||||
},
|
||||
"button": {
|
||||
"start": "Start transcribing the meeting",
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"messageReceived": {
|
||||
"label": "Message received"
|
||||
},
|
||||
"participantWaiting": {
|
||||
"label": "Participant waiting"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -479,7 +479,7 @@
|
||||
"destination": "Se creará un nuevo documento en",
|
||||
"destinationUnknown": "Se creará un nuevo documento",
|
||||
"language": "Idioma de la reunión:",
|
||||
"recording": "Iniciar también una grabación"
|
||||
"recording": "Iniciar también una grabación de vídeo"
|
||||
},
|
||||
"button": {
|
||||
"start": "Empezar a transcribir la reunión",
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"messageReceived": {
|
||||
"label": "Un mensaje recibido"
|
||||
},
|
||||
"participantWaiting": {
|
||||
"label": "Una persona en la sala de espera"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "Un nouveau document sera créé sur",
|
||||
"destinationUnknown": "Un nouveau document sera créé",
|
||||
"language": "Langue de la réunion :",
|
||||
"recording": "Démarrer aussi un enregistrement"
|
||||
"recording": "Démarrer aussi un enregistrement vidéo"
|
||||
},
|
||||
"button": {
|
||||
"start": "Commencer à transcrire la réunion",
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"messageReceived": {
|
||||
"label": "Un message reçu"
|
||||
},
|
||||
"participantWaiting": {
|
||||
"label": "Une personne en salle d’attente"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "Er wordt een nieuw document aangemaakt op",
|
||||
"destinationUnknown": "Een nieuw document wordt aangemaakt",
|
||||
"language": "Vergadertalen:",
|
||||
"recording": "Start ook een opname"
|
||||
"recording": "Start ook een video-opname"
|
||||
},
|
||||
"button": {
|
||||
"start": "Begin met het transcriberen van de vergadering",
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"messageReceived": {
|
||||
"label": "Bericht ontvangen"
|
||||
},
|
||||
"participantWaiting": {
|
||||
"label": "Deelnemer in de wachtkamer"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ const DEFAULT_STATE: State = {
|
||||
[NotificationType.ParticipantJoined, true],
|
||||
[NotificationType.HandRaised, true],
|
||||
[NotificationType.MessageReceived, true],
|
||||
[NotificationType.ParticipantWaiting, true],
|
||||
])
|
||||
),
|
||||
soundNotificationVolume: 0.1,
|
||||
|
||||
@@ -150,6 +150,7 @@ backend:
|
||||
# Frontend
|
||||
LIVEKIT_FORCE_WSS_PROTOCOL: True
|
||||
LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND: True
|
||||
LIVEKIT_DEFAULT_VIDEO_CODEC: vp9
|
||||
FRONTEND_IDLE_DISCONNECT_WARNING_DELAY: 9000
|
||||
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
|
||||
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041', 'help_article_transcript': 'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x', 'help_article_recording': 'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0', 'help_article_more_tools': 'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23'}"
|
||||
@@ -255,37 +256,6 @@ posthog:
|
||||
ingressAssets:
|
||||
enabled: false
|
||||
|
||||
# ---- Extra ingress/service for recording file downloads -----------
|
||||
|
||||
ingressMedia:
|
||||
enabled: true
|
||||
host: meet.127.0.0.1.nip.io
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/recordings/media-auth/
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: minio.meet.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/$1
|
||||
|
||||
serviceMedia:
|
||||
host: minio.meet.svc.cluster.local
|
||||
port: 9000
|
||||
|
||||
# ---- Extra ingress/service for background file uploads ------------
|
||||
|
||||
ingressMediaFiles:
|
||||
enabled: true
|
||||
host: meet.127.0.0.1.nip.io
|
||||
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/files/media-auth/
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: minio.meet.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/files/$1
|
||||
|
||||
serviceMediaFiles:
|
||||
host: minio.meet.svc.cluster.local
|
||||
port: 9000
|
||||
|
||||
# ---- STT Orchestration Microservice Components --------------------
|
||||
|
||||
summary:
|
||||
|
||||
@@ -23,9 +23,6 @@ frontend:
|
||||
- name: outlook-addon-manifest
|
||||
configMap:
|
||||
name: outlook-addon-manifest
|
||||
- name: frontend-nginx-config
|
||||
configMap:
|
||||
name: frontend-nginx-config
|
||||
|
||||
extraVolumeMounts:
|
||||
- name: outlook-addon-config
|
||||
@@ -36,10 +33,6 @@ frontend:
|
||||
mountPath: /usr/share/nginx/html/addons/outlook/manifest.xml
|
||||
subPath: manifest.xml
|
||||
readOnly: true
|
||||
- name: frontend-nginx-config
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: default.conf
|
||||
readOnly: true
|
||||
|
||||
outlookAddon:
|
||||
enabled: true
|
||||
@@ -49,5 +42,3 @@ frontend:
|
||||
appName: "Visio"
|
||||
id: "a025f0f6-757a-4790-97f3-99c66c4a5795"
|
||||
|
||||
frontendNginxConfig:
|
||||
enabled: true
|
||||
|
||||
@@ -55,5 +55,3 @@ agentSubtitles:
|
||||
- key: cacert.pem
|
||||
path: cert.pem
|
||||
|
||||
frontendNginxConfig:
|
||||
enabled: false
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
{{- if .Values.frontendNginxConfig.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: frontend-nginx-config
|
||||
namespace: {{ .Release.Namespace }}
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
server_tokens off;
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
# Manifest — fetched, never iframed
|
||||
location = /addons/outlook/manifest.xml {
|
||||
alias /usr/share/nginx/html/addons/outlook/manifest.xml;
|
||||
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header X-Frame-Options "DENY";
|
||||
add_header Content-Security-Policy "frame-ancestors 'none'";
|
||||
}
|
||||
|
||||
location = /addons/outlook/assets/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~* ^/addons/outlook/assets/(.+\.(?:css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot))/?$ {
|
||||
root /usr/share/nginx/html;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000, immutable" always;
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Vary "Origin" always;
|
||||
}
|
||||
|
||||
location = /addons/outlook/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ ^/addons/outlook(/.*)?$ {
|
||||
alias /usr/share/nginx/html/addons/outlook$1;
|
||||
error_page 404 =200 /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires 0 always;
|
||||
|
||||
set $ms_domains "https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com";
|
||||
|
||||
set $nonce $request_id;
|
||||
|
||||
set $csp "default-src 'self'; upgrade-insecure-requests; ";
|
||||
set $csp "${csp}frame-ancestors ${ms_domains}; ";
|
||||
set $csp "${csp}script-src 'nonce-${nonce}' 'strict-dynamic'; ";
|
||||
set $csp "${csp}style-src 'self' 'unsafe-inline'; ";
|
||||
set $csp "${csp}img-src 'self' data:; ";
|
||||
set $csp "${csp}font-src 'self' data:; ";
|
||||
set $csp "${csp}connect-src 'self' ${ms_domains}; ";
|
||||
set $csp "${csp}frame-src 'none'; ";
|
||||
set $csp "${csp}object-src 'none'; ";
|
||||
set $csp "${csp}base-uri 'none'; ";
|
||||
|
||||
add_header Content-Security-Policy $csp;
|
||||
|
||||
sub_filter 'NONCE_PLACEHOLDER' $nonce;
|
||||
sub_filter_once off;
|
||||
}
|
||||
|
||||
location ^~ /assets/mediapipe/wasm/ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
# 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;
|
||||
}
|
||||
{{- end }}
|
||||
@@ -74,7 +74,7 @@ spec:
|
||||
value: meet
|
||||
- name: MINIO_ROOT_PASSWORD
|
||||
value: password
|
||||
image: "minio/minio"
|
||||
image: "quay.io/minio/minio"
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 9000
|
||||
@@ -103,7 +103,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: mc
|
||||
image: minio/mc
|
||||
image: quay.io/minio/mc
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
|
||||
+5
-19
@@ -34,24 +34,6 @@
|
||||
| `ingressAdmin.tls.secretName` | Secret name for TLS config | `nil` |
|
||||
| `ingressAdmin.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingressAdmin.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
| `ingressMedia.enabled` | whether to enable the Ingress or not | `false` |
|
||||
| `ingressMedia.className` | IngressClass to use for the Ingress | `nil` |
|
||||
| `ingressMedia.host` | Host for the Ingress | `meet.example.com` |
|
||||
| `ingressMedia.path` | Path to use for the Ingress | `/media/(.*)` |
|
||||
| `ingressMedia.hosts` | Additional host to configure for the Ingress | `[]` |
|
||||
| `ingressMedia.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
|
||||
| `ingressMedia.tls.secretName` | Secret name for TLS config | `nil` |
|
||||
| `ingressMedia.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingressMedia.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-url` | | `https://meet.example.com/api/v1.0/recordings/media-auth/` |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-response-headers` | | `Authorization, X-Amz-Date, X-Amz-Content-SHA256` |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/upstream-vhost` | | `minio.meet.svc.cluster.local:9000` |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/configuration-snippet` | | `add_header Content-Security-Policy "default-src 'none'" always;
|
||||
` |
|
||||
| `serviceMedia.host` | | `minio.meet.svc.cluster.local` |
|
||||
| `serviceMedia.port` | | `9000` |
|
||||
| `serviceMedia.annotations` | | `{}` |
|
||||
|
||||
### backend
|
||||
|
||||
| Name | Description | Value |
|
||||
@@ -125,7 +107,11 @@
|
||||
| `frontend.envVars.FROM_CONFIGMAP.configMapKeyRef.key` | Key within a ConfigMap when configuring env vars from a ConfigMap | |
|
||||
| `frontend.envVars.FROM_SECRET.secretKeyRef.name` | Name of a Secret when configuring env vars from a Secret | |
|
||||
| `frontend.envVars.FROM_SECRET.secretKeyRef.key` | Key within a Secret when configuring env vars from a Secret | |
|
||||
| `frontend.podAnnotations` | Annotations to add to the frontend Pod | `{}` |
|
||||
| `frontend.podAnnotations` | Annotations to add to the frontend Pod | `{}` | |
|
||||
| `frontend.mediaProxy.storageHost` | Hostname of the S3/MinIO endpoint serving recordings and files | `minio.meet.svc.cluster.local` |
|
||||
| `frontend.mediaProxy.storageProtocol` | The protocol of the S3/MinIO endpoint serving recordings and files | http |
|
||||
| `frontend.mediaProxy.storagePort` | Port of the S3/MinIO endpoint serving recordings and files | `9000` |
|
||||
| `frontend.mediaProxy.bucketName` | Name of the S3/MinIO bucket storing recordings and files | `meet-media-storage` |
|
||||
| `frontend.service.type` | frontend Service type | `ClusterIP` |
|
||||
| `frontend.service.port` | frontend Service listening port | `80` |
|
||||
| `frontend.service.targetPort` | frontend container listening port | `8080` |
|
||||
|
||||
@@ -54,6 +54,18 @@ spec:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
{{- end }}
|
||||
- name: BACKEND_INTERNAL_HOST
|
||||
value: {{ include "meet.backend.fullname" . | quote }}
|
||||
- name: BACKEND_INTERNAL_PORT
|
||||
value: {{ .Values.backend.service.port | quote }}
|
||||
- name: MEDIA_STORAGE_HOST
|
||||
value: {{ .Values.frontend.mediaProxy.storageHost | quote }}
|
||||
- name: MEDIA_STORAGE_PROTOCOL
|
||||
value: {{ .Values.frontend.mediaProxy.storageProtocol | quote }}
|
||||
- name: MEDIA_STORAGE_PORT
|
||||
value: {{ .Values.frontend.mediaProxy.storagePort | quote }}
|
||||
- name: AWS_STORAGE_BUCKET_NAME
|
||||
value: {{ .Values.frontend.mediaProxy.bucketName | quote }}
|
||||
{{- with .Values.frontend.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
{{- if .Values.ingressMedia.enabled -}}
|
||||
{{- $fullName := include "meet.fullname" . -}}
|
||||
{{- if and .Values.ingressMedia.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingressMedia.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingressMedia.annotations "kubernetes.io/ingress.class" .Values.ingressMedia.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}-media
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "meet.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingressMedia.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingressMedia.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingressMedia.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingressMedia.tls.enabled }}
|
||||
tls:
|
||||
{{- if .Values.ingressMedia.host }}
|
||||
- secretName: {{ .Values.ingressMedia.tls.secretName | default (printf "%s-tls" $fullName) | quote }}
|
||||
hosts:
|
||||
- {{ .Values.ingressMedia.host | quote }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressMedia.tls.additional }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- if .Values.ingressMedia.host }}
|
||||
- host: {{ .Values.ingressMedia.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingressMedia.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: ImplementationSpecific
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}-media
|
||||
port:
|
||||
number: {{ .Values.serviceMedia.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}-media
|
||||
servicePort: {{ .Values.serviceMedia.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressMedia.hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $.Values.ingressMedia.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: ImplementationSpecific
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}-media
|
||||
port:
|
||||
number: {{ .Values.serviceMedia.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}-media
|
||||
servicePort: {{ .Values.serviceMedia.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,83 +0,0 @@
|
||||
{{- if .Values.ingressMediaFiles.enabled -}}
|
||||
{{- $fullName := include "meet.fullname" . -}}
|
||||
{{- if and .Values.ingressMediaFiles.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingressMediaFiles.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingressMediaFiles.annotations "kubernetes.io/ingress.class" .Values.ingressMediaFiles.className }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}-media-files
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "meet.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingressMediaFiles.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingressMediaFiles.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingressMediaFiles.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingressMediaFiles.tls.enabled }}
|
||||
tls:
|
||||
{{- if .Values.ingressMediaFiles.host }}
|
||||
- secretName: {{ .Values.ingressMediaFiles.tls.secretName | default (printf "%s-tls" $fullName) | quote }}
|
||||
hosts:
|
||||
- {{ .Values.ingressMediaFiles.host | quote }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressMediaFiles.tls.additional }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- if .Values.ingressMediaFiles.host }}
|
||||
- host: {{ .Values.ingressMediaFiles.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingressMediaFiles.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: ImplementationSpecific
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}-media-files
|
||||
port:
|
||||
number: {{ .Values.serviceMediaFiles.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}-media-files
|
||||
servicePort: {{ .Values.serviceMediaFiles.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressMediaFiles.hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $.Values.ingressMediaFiles.path | quote }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: ImplementationSpecific
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}-media-files
|
||||
port:
|
||||
number: {{ .Values.serviceMediaFiles.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}-media-files
|
||||
servicePort: {{ .Values.serviceMediaFiles.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,14 +0,0 @@
|
||||
{{- $fullName := include "meet.fullname" . -}}
|
||||
{{- $component := "media-files" -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $fullName }}-media-files
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "meet.common.labels" (list . $component) | nindent 4 }}
|
||||
annotations:
|
||||
{{- toYaml $.Values.serviceMediaFiles.annotations | nindent 4 }}
|
||||
spec:
|
||||
type: ExternalName
|
||||
externalName: {{ $.Values.serviceMediaFiles.host }}
|
||||
@@ -1,14 +0,0 @@
|
||||
{{- $fullName := include "meet.fullname" . -}}
|
||||
{{- $component := "media" -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $fullName }}-media
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "meet.common.labels" (list . $component) | nindent 4 }}
|
||||
annotations:
|
||||
{{- toYaml $.Values.serviceMedia.annotations | nindent 4 }}
|
||||
spec:
|
||||
type: ExternalName
|
||||
externalName: {{ $.Values.serviceMedia.host }}
|
||||
+10
-88
@@ -98,94 +98,6 @@ ingressAdmin:
|
||||
enabled: true
|
||||
additional: []
|
||||
|
||||
## @param ingressMedia.enabled whether to enable the Ingress or not
|
||||
## @param ingressMedia.className IngressClass to use for the Ingress
|
||||
## @param ingressMedia.host Host for the Ingress
|
||||
## @param ingressMedia.path Path to use for the Ingress
|
||||
ingressMedia:
|
||||
enabled: false
|
||||
className: null
|
||||
host: meet.example.com
|
||||
path: /media/(.*)
|
||||
## @param ingressMedia.hosts Additional host to configure for the Ingress
|
||||
hosts: [ ]
|
||||
# - chart-example.local
|
||||
## @param ingressMedia.tls.enabled Whether to enable TLS for the Ingress
|
||||
## @param ingressMedia.tls.secretName Secret name for TLS config
|
||||
## @skip ingressMedia.tls.additional
|
||||
## @extra ingressMedia.tls.additional[].secretName Secret name for additional TLS config
|
||||
## @extra ingressMedia.tls.additional[].hosts[] Hosts for additional TLS config
|
||||
tls:
|
||||
secretName: null
|
||||
enabled: true
|
||||
additional: []
|
||||
|
||||
## @param ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-url
|
||||
## @param ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-response-headers
|
||||
## @param ingressMedia.annotations.nginx.ingress.kubernetes.io/upstream-vhost
|
||||
## @param ingressMedia.annotations.nginx.ingress.kubernetes.io/configuration-snippet
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||
nginx.ingress.kubernetes.io/auth-url: https://meet.example.com/api/v1.0/recordings/media-auth/
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: minio.meet.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
add_header Content-Security-Policy "default-src 'none'" always;
|
||||
|
||||
## @param serviceMedia.host
|
||||
## @param serviceMedia.port
|
||||
## @param serviceMedia.annotations
|
||||
serviceMedia:
|
||||
host: minio.meet.svc.cluster.local
|
||||
port: 9000
|
||||
annotations: {}
|
||||
|
||||
|
||||
## @param ingressMediaFiles.enabled whether to enable the Ingress or not
|
||||
## @param ingressMediaFiles.className IngressClass to use for the Ingress
|
||||
## @param ingressMediaFiles.host Host for the Ingress
|
||||
## @param ingressMediaFiles.path Path to use for the Ingress
|
||||
ingressMediaFiles:
|
||||
enabled: false
|
||||
className: null
|
||||
host: meet.example.com
|
||||
path: /media/files/(.*)
|
||||
## @param ingressMediaFiles.hosts Additional host to configure for the Ingress
|
||||
hosts: [ ]
|
||||
# - chart-example.local
|
||||
## @param ingressMediaFiles.tls.enabled Weather to enable TLS for the Ingress
|
||||
## @param ingressMediaFiles.tls.secretName Secret name for TLS config
|
||||
## @skip ingressMediaFiles.tls.additional
|
||||
## @extra ingressMediaFiles.tls.additional[].secretName Secret name for additional TLS config
|
||||
## @extra ingressMediaFiles.tls.additional[].hosts[] Hosts for additional TLS config
|
||||
tls:
|
||||
secretName: null
|
||||
enabled: true
|
||||
additional: []
|
||||
|
||||
## @param ingressMediaFiles.annotations.nginx.ingress.kubernetes.io/auth-url
|
||||
## @param ingressMediaFiles.annotations.nginx.ingress.kubernetes.io/auth-response-headers
|
||||
## @param ingressMediaFiles.annotations.nginx.ingress.kubernetes.io/upstream-vhost
|
||||
## @param ingressMediaFiles.annotations.nginx.ingress.kubernetes.io/configuration-snippet
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||
nginx.ingress.kubernetes.io/auth-url: https://meet.example.com/api/v1.0/files/media-auth/
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: minio.meet.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
add_header Content-Security-Policy "default-src 'none'" always;
|
||||
add_header Content-Disposition "attachment";
|
||||
|
||||
## @param serviceMediaFiles.host
|
||||
## @param serviceMediaFiles.port
|
||||
## @param serviceMediaFiles.annotations
|
||||
serviceMediaFiles:
|
||||
host: minio.meet.svc.cluster.local
|
||||
port: 9000
|
||||
annotations: {}
|
||||
|
||||
|
||||
|
||||
## @section backend
|
||||
|
||||
backend:
|
||||
@@ -417,6 +329,16 @@ frontend:
|
||||
## @param frontend.podAnnotations Annotations to add to the frontend Pod
|
||||
podAnnotations: {}
|
||||
|
||||
## @param frontend.mediaProxy.storageHost Hostname of the S3/MinIO endpoint serving recordings and files
|
||||
## @param frontend.mediaProxy.storageProtocol Protocol of the S3/MinIO endpoint serving recordings and files
|
||||
## @param frontend.mediaProxy.storagePort Port of the S3/MinIO endpoint serving recordings and files
|
||||
## @param frontend.mediaProxy.bucketName Name of the S3/MinIO bucket storing recordings and files
|
||||
mediaProxy:
|
||||
storageHost: minio.meet.svc.cluster.local
|
||||
storageProtocol: http
|
||||
storagePort: 9000
|
||||
bucketName: meet-media-storage
|
||||
|
||||
## @param frontend.service.type frontend Service type
|
||||
## @param frontend.service.port frontend Service listening port
|
||||
## @param frontend.service.targetPort frontend container listening port
|
||||
|
||||
Reference in New Issue
Block a user